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-2017 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 "HTMLOptionElement.h"
29
30#include "Document.h"
31#include "HTMLDataListElement.h"
32#include "HTMLNames.h"
33#include "HTMLOptGroupElement.h"
34#include "HTMLParserIdioms.h"
35#include "HTMLSelectElement.h"
36#include "NodeRenderStyle.h"
37#include "NodeTraversal.h"
38#include "RenderMenuList.h"
39#include "RenderTheme.h"
40#include "ScriptElement.h"
41#include "StyleResolver.h"
42#include "Text.h"
43#include <wtf/IsoMallocInlines.h>
44#include <wtf/Ref.h>
45
46namespace WebCore {
47
48WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLOptionElement);
49
50using namespace HTMLNames;
51
52HTMLOptionElement::HTMLOptionElement(const QualifiedName& tagName, Document& document)
53 : HTMLElement(tagName, document)
54 , m_disabled(false)
55 , m_isSelected(false)
56{
57 ASSERT(hasTagName(optionTag));
58 setHasCustomStyleResolveCallbacks();
59}
60
61Ref<HTMLOptionElement> HTMLOptionElement::create(Document& document)
62{
63 return adoptRef(*new HTMLOptionElement(optionTag, document));
64}
65
66Ref<HTMLOptionElement> HTMLOptionElement::create(const QualifiedName& tagName, Document& document)
67{
68 return adoptRef(*new HTMLOptionElement(tagName, document));
69}
70
71ExceptionOr<Ref<HTMLOptionElement>> HTMLOptionElement::createForJSConstructor(Document& document, const String& text, const String& value, bool defaultSelected, bool selected)
72{
73 auto element = create(document);
74
75 if (!text.isEmpty()) {
76 auto appendResult = element->appendChild(Text::create(document, text));
77 if (appendResult.hasException())
78 return appendResult.releaseException();
79 }
80
81 if (!value.isNull())
82 element->setValue(value);
83 if (defaultSelected)
84 element->setAttributeWithoutSynchronization(selectedAttr, emptyAtom());
85 element->setSelected(selected);
86
87 return element;
88}
89
90bool HTMLOptionElement::isFocusable() const
91{
92 if (!supportsFocus())
93 return false;
94 // Option elements do not have a renderer.
95 auto* style = const_cast<HTMLOptionElement&>(*this).computedStyle();
96 return style && style->display() != DisplayType::None;
97}
98
99bool HTMLOptionElement::matchesDefaultPseudoClass() const
100{
101 return hasAttributeWithoutSynchronization(selectedAttr);
102}
103
104String HTMLOptionElement::text() const
105{
106 String text = collectOptionInnerText();
107
108 // FIXME: Is displayStringModifiedByEncoding helpful here?
109 // If it's correct here, then isn't it needed in the value and label functions too?
110 return stripLeadingAndTrailingHTMLSpaces(document().displayStringModifiedByEncoding(text)).simplifyWhiteSpace(isHTMLSpace);
111}
112
113void HTMLOptionElement::setText(const String &text)
114{
115 Ref<HTMLOptionElement> protectedThis(*this);
116
117 // Changing the text causes a recalc of a select's items, which will reset the selected
118 // index to the first item if the select is single selection with a menu list. We attempt to
119 // preserve the selected item.
120 RefPtr<HTMLSelectElement> select = ownerSelectElement();
121 bool selectIsMenuList = select && select->usesMenuList();
122 int oldSelectedIndex = selectIsMenuList ? select->selectedIndex() : -1;
123
124 // Handle the common special case where there's exactly 1 child node, and it's a text node.
125 RefPtr<Node> child = firstChild();
126 if (is<Text>(child) && !child->nextSibling())
127 downcast<Text>(*child).setData(text);
128 else {
129 removeChildren();
130 appendChild(Text::create(document(), text));
131 }
132
133 if (selectIsMenuList && select->selectedIndex() != oldSelectedIndex)
134 select->setSelectedIndex(oldSelectedIndex);
135}
136
137void HTMLOptionElement::accessKeyAction(bool)
138{
139 RefPtr<HTMLSelectElement> select = ownerSelectElement();
140 if (select)
141 select->accessKeySetSelectedIndex(index());
142}
143
144int HTMLOptionElement::index() const
145{
146 // It would be faster to cache the index, but harder to get it right in all cases.
147
148 RefPtr<HTMLSelectElement> selectElement = ownerSelectElement();
149 if (!selectElement)
150 return 0;
151
152 int optionIndex = 0;
153
154 for (auto& item : selectElement->listItems()) {
155 if (!is<HTMLOptionElement>(*item))
156 continue;
157 if (item == this)
158 return optionIndex;
159 ++optionIndex;
160 }
161
162 return 0;
163}
164
165void HTMLOptionElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
166{
167#if ENABLE(DATALIST_ELEMENT)
168 if (name == valueAttr) {
169 if (RefPtr<HTMLDataListElement> dataList = ownerDataListElement())
170 dataList->optionElementChildrenChanged();
171 } else
172#endif
173 if (name == disabledAttr) {
174 bool oldDisabled = m_disabled;
175 m_disabled = !value.isNull();
176 if (oldDisabled != m_disabled) {
177 invalidateStyleForSubtree();
178 if (renderer() && renderer()->style().hasAppearance())
179 renderer()->theme().stateChanged(*renderer(), ControlStates::EnabledState);
180 }
181 } else if (name == selectedAttr) {
182 invalidateStyleForSubtree();
183
184 // FIXME: This doesn't match what the HTML specification says.
185 // The specification implies that removing the selected attribute or
186 // changing the value of a selected attribute that is already present
187 // has no effect on whether the element is selected. Further, it seems
188 // that we need to do more than just set m_isSelected to select in that
189 // case; we'd need to do the other work from the setSelected function.
190 m_isSelected = !value.isNull();
191 } else
192 HTMLElement::parseAttribute(name, value);
193}
194
195String HTMLOptionElement::value() const
196{
197 const AtomicString& value = attributeWithoutSynchronization(valueAttr);
198 if (!value.isNull())
199 return value;
200 return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace);
201}
202
203void HTMLOptionElement::setValue(const String& value)
204{
205 setAttributeWithoutSynchronization(valueAttr, value);
206}
207
208bool HTMLOptionElement::selected()
209{
210 if (RefPtr<HTMLSelectElement> select = ownerSelectElement())
211 select->updateListItemSelectedStates();
212 return m_isSelected;
213}
214
215void HTMLOptionElement::setSelected(bool selected)
216{
217 if (m_isSelected == selected)
218 return;
219
220 setSelectedState(selected);
221
222 if (RefPtr<HTMLSelectElement> select = ownerSelectElement())
223 select->optionSelectionStateChanged(*this, selected);
224}
225
226void HTMLOptionElement::setSelectedState(bool selected)
227{
228 if (m_isSelected == selected)
229 return;
230
231 m_isSelected = selected;
232 invalidateStyleForSubtree();
233
234 if (RefPtr<HTMLSelectElement> select = ownerSelectElement())
235 select->invalidateSelectedItems();
236}
237
238void HTMLOptionElement::childrenChanged(const ChildChange& change)
239{
240#if ENABLE(DATALIST_ELEMENT)
241 if (RefPtr<HTMLDataListElement> dataList = ownerDataListElement())
242 dataList->optionElementChildrenChanged();
243 else
244#endif
245 if (RefPtr<HTMLSelectElement> select = ownerSelectElement())
246 select->optionElementChildrenChanged();
247 HTMLElement::childrenChanged(change);
248}
249
250#if ENABLE(DATALIST_ELEMENT)
251HTMLDataListElement* HTMLOptionElement::ownerDataListElement() const
252{
253 RefPtr<ContainerNode> datalist = parentNode();
254 while (datalist && !is<HTMLDataListElement>(*datalist))
255 datalist = datalist->parentNode();
256
257 if (!datalist)
258 return nullptr;
259
260 return downcast<HTMLDataListElement>(datalist.get());
261}
262#endif
263
264HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const
265{
266 RefPtr<ContainerNode> select = parentNode();
267 while (select && !is<HTMLSelectElement>(*select))
268 select = select->parentNode();
269
270 if (!select)
271 return nullptr;
272
273 return downcast<HTMLSelectElement>(select.get());
274}
275
276String HTMLOptionElement::label() const
277{
278 String label = attributeWithoutSynchronization(labelAttr);
279 if (!label.isNull())
280 return stripLeadingAndTrailingHTMLSpaces(label);
281 return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace);
282}
283
284// Same as label() but ignores the label content attribute in quirks mode for compatibility with other browsers.
285String HTMLOptionElement::displayLabel() const
286{
287 if (document().inQuirksMode())
288 return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace);
289 return label();
290}
291
292void HTMLOptionElement::setLabel(const String& label)
293{
294 setAttributeWithoutSynchronization(labelAttr, label);
295}
296
297void HTMLOptionElement::willResetComputedStyle()
298{
299 // FIXME: This is nasty, we ask our owner select to repaint even if the new
300 // style is exactly the same.
301 if (auto select = ownerSelectElement()) {
302 if (auto renderer = select->renderer())
303 renderer->repaint();
304 }
305}
306
307String HTMLOptionElement::textIndentedToRespectGroupLabel() const
308{
309 RefPtr<ContainerNode> parent = parentNode();
310 if (is<HTMLOptGroupElement>(parent))
311 return " " + displayLabel();
312 return displayLabel();
313}
314
315bool HTMLOptionElement::isDisabledFormControl() const
316{
317 if (ownElementDisabled())
318 return true;
319
320 if (!is<HTMLOptGroupElement>(parentNode()))
321 return false;
322
323 return downcast<HTMLOptGroupElement>(*parentNode()).isDisabledFormControl();
324}
325
326Node::InsertedIntoAncestorResult HTMLOptionElement::insertedIntoAncestor(InsertionType insertionType, ContainerNode& parentOfInsertedTree)
327{
328 if (RefPtr<HTMLSelectElement> select = ownerSelectElement()) {
329 select->setRecalcListItems();
330 select->updateValidity();
331 // Do not call selected() since calling updateListItemSelectedStates()
332 // at this time won't do the right thing. (Why, exactly?)
333 // FIXME: Might be better to call this unconditionally, always passing m_isSelected,
334 // rather than only calling it if we are selected.
335 if (m_isSelected)
336 select->optionSelectionStateChanged(*this, true);
337 select->scrollToSelection();
338 }
339
340 return HTMLElement::insertedIntoAncestor(insertionType, parentOfInsertedTree);
341}
342
343String HTMLOptionElement::collectOptionInnerText() const
344{
345 StringBuilder text;
346 for (RefPtr<Node> node = firstChild(); node; ) {
347 if (is<Text>(*node))
348 text.append(node->nodeValue());
349 // Text nodes inside script elements are not part of the option text.
350 if (is<Element>(*node) && isScriptElement(downcast<Element>(*node)))
351 node = NodeTraversal::nextSkippingChildren(*node, this);
352 else
353 node = NodeTraversal::next(*node, this);
354 }
355 return text.toString();
356}
357
358} // namespace
359