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 | |
46 | namespace WebCore { |
47 | |
48 | WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLOptionElement); |
49 | |
50 | using namespace HTMLNames; |
51 | |
52 | HTMLOptionElement::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 | |
61 | Ref<HTMLOptionElement> HTMLOptionElement::create(Document& document) |
62 | { |
63 | return adoptRef(*new HTMLOptionElement(optionTag, document)); |
64 | } |
65 | |
66 | Ref<HTMLOptionElement> HTMLOptionElement::create(const QualifiedName& tagName, Document& document) |
67 | { |
68 | return adoptRef(*new HTMLOptionElement(tagName, document)); |
69 | } |
70 | |
71 | ExceptionOr<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 | |
90 | bool 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 | |
99 | bool HTMLOptionElement::matchesDefaultPseudoClass() const |
100 | { |
101 | return hasAttributeWithoutSynchronization(selectedAttr); |
102 | } |
103 | |
104 | String 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 | |
113 | void 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 = 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 | |
137 | void HTMLOptionElement::accessKeyAction(bool) |
138 | { |
139 | RefPtr<HTMLSelectElement> select = ownerSelectElement(); |
140 | if (select) |
141 | select->accessKeySetSelectedIndex(index()); |
142 | } |
143 | |
144 | int 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 | |
165 | void 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 | |
195 | String 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 | |
203 | void HTMLOptionElement::setValue(const String& value) |
204 | { |
205 | setAttributeWithoutSynchronization(valueAttr, value); |
206 | } |
207 | |
208 | bool HTMLOptionElement::selected() |
209 | { |
210 | if (RefPtr<HTMLSelectElement> select = ownerSelectElement()) |
211 | select->updateListItemSelectedStates(); |
212 | return m_isSelected; |
213 | } |
214 | |
215 | void 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 | |
226 | void 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 | |
238 | void 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) |
251 | HTMLDataListElement* 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 | |
264 | HTMLSelectElement* 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 | |
276 | String 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. |
285 | String HTMLOptionElement::displayLabel() const |
286 | { |
287 | if (document().inQuirksMode()) |
288 | return stripLeadingAndTrailingHTMLSpaces(collectOptionInnerText()).simplifyWhiteSpace(isHTMLSpace); |
289 | return label(); |
290 | } |
291 | |
292 | void HTMLOptionElement::setLabel(const String& label) |
293 | { |
294 | setAttributeWithoutSynchronization(labelAttr, label); |
295 | } |
296 | |
297 | void 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 | |
307 | String HTMLOptionElement::textIndentedToRespectGroupLabel() const |
308 | { |
309 | RefPtr<ContainerNode> parent = parentNode(); |
310 | if (is<HTMLOptGroupElement>(parent)) |
311 | return " " + displayLabel(); |
312 | return displayLabel(); |
313 | } |
314 | |
315 | bool 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 | |
326 | Node::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 | |
343 | String 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 | |