blob: 4c2afcc152674290a525eeea9c151d5ab4840bc1 [file] [log] [blame]
/*
* Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 1999 Antti Koivisto (koivisto@kde.org)
* (C) 2001 Dirk Mueller (mueller@kde.org)
* Copyright (C) 2004, 2005, 2006, 2007, 2009, 2010, 2011 Apple Inc. All rights
* reserved.
* (C) 2006 Alexey Proskuryakov (ap@nypop.com)
* Copyright (C) 2010 Google Inc. All rights reserved.
* Copyright (C) 2009 Torch Mobile Inc. All rights reserved.
* (http://www.torchmobile.com/)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "build/build_config.h"
#include "third_party/blink/public/mojom/input/focus_type.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/strings/grit/blink_strings.h"
#include "third_party/blink/renderer/bindings/core/v8/html_element_or_long.h"
#include "third_party/blink/renderer/bindings/core/v8/html_option_element_or_html_opt_group_element.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/css/style_change_reason.h"
#include "third_party/blink/renderer/core/dom/attribute.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
#include "third_party/blink/renderer/core/dom/events/simulated_click_options.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/node_lists_node_data.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/html/forms/form_controller.h"
#include "third_party/blink/renderer/core/html/forms/form_data.h"
#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "third_party/blink/renderer/core/html/forms/html_opt_group_element.h"
#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
#include "third_party/blink/renderer/core/html/forms/select_type.h"
#include "third_party/blink/renderer/core/html/html_hr_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/layout/hit_test_request.h"
#include "third_party/blink/renderer/core/layout/hit_test_result.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/layout_object_factory.h"
#include "third_party/blink/renderer/core/layout/layout_theme.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/spatial_navigation.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "ui/base/ui_base_features.h"
namespace blink {
// Upper limit of list_items_. According to the HTML standard, options larger
// than this limit doesn't work well because |selectedIndex| IDL attribute is
// signed.
static const unsigned kMaxListItems = INT_MAX;
// Default size when the multiple attribute is present but size attribute is
// absent.
const int kDefaultListBoxSize = 4;
HTMLSelectElement::HTMLSelectElement(Document& document)
: HTMLFormControlElementWithState(html_names::kSelectTag, document),
type_ahead_(this),
size_(0),
last_on_change_option_(nullptr),
is_multiple_(false),
should_recalc_list_items_(false),
is_autofilled_by_preview_(false),
index_to_select_on_cancel_(-1) {
// Make sure SelectType is created after initializing |uses_menu_list_|.
select_type_ = SelectType::Create(*this);
SetHasCustomStyleCallbacks();
EnsureUserAgentShadowRoot();
}
HTMLSelectElement::~HTMLSelectElement() = default;
// static
bool HTMLSelectElement::CanAssignToSelectSlot(const Node& node) {
// Even if options/optgroups are not rendered as children of menulist SELECT,
// we still need to add them to the flat tree through slotting since we need
// their ComputedStyle for popup rendering.
return node.HasTagName(html_names::kOptionTag) ||
node.HasTagName(html_names::kOptgroupTag) ||
node.HasTagName(html_names::kHrTag);
}
const AtomicString& HTMLSelectElement::FormControlType() const {
DEFINE_STATIC_LOCAL(const AtomicString, select_multiple, ("select-multiple"));
DEFINE_STATIC_LOCAL(const AtomicString, select_one, ("select-one"));
return is_multiple_ ? select_multiple : select_one;
}
bool HTMLSelectElement::HasPlaceholderLabelOption() const {
// The select element has no placeholder label option if it has an attribute
// "multiple" specified or a display size of non-1.
//
// The condition "size() > 1" is not compliant with the HTML5 spec as of Dec
// 3, 2010. "size() != 1" is correct. Using "size() > 1" here because
// size() may be 0 in WebKit. See the discussion at
// https://bugs.webkit.org/show_bug.cgi?id=43887
//
// "0 size()" happens when an attribute "size" is absent or an invalid size
// attribute is specified. In this case, the display size should be assumed
// as the default. The default display size is 1 for non-multiple select
// elements, and 4 for multiple select elements.
//
// Finally, if size() == 0 and non-multiple, the display size can be assumed
// as 1.
if (IsMultiple() || size() > 1)
return false;
// TODO(tkent): This function is called in CSS selector matching. Using
// listItems() might have performance impact.
if (GetListItems().size() == 0)
return false;
auto* option_element = DynamicTo<HTMLOptionElement>(GetListItems()[0].Get());
if (!option_element)
return false;
return option_element->value().IsEmpty();
}
String HTMLSelectElement::validationMessage() const {
if (!willValidate())
return String();
if (CustomError())
return CustomValidationMessage();
if (ValueMissing()) {
return GetLocale().QueryString(IDS_FORM_VALIDATION_VALUE_MISSING_SELECT);
}
return String();
}
bool HTMLSelectElement::ValueMissing() const {
if (!IsRequired())
return false;
int first_selection_index = selectedIndex();
// If a non-placeholer label option is selected (firstSelectionIndex > 0),
// it's not value-missing.
return first_selection_index < 0 ||
(!first_selection_index && HasPlaceholderLabelOption());
}
String HTMLSelectElement::DefaultToolTip() const {
if (Form() && Form()->NoValidate())
return String();
return validationMessage();
}
void HTMLSelectElement::SelectMultipleOptionsByPopup(
const Vector<int>& list_indices) {
DCHECK(UsesMenuList());
DCHECK(IsMultiple());
HeapHashSet<Member<HTMLOptionElement>> old_selection;
for (auto* option : GetOptionList()) {
if (option->Selected()) {
old_selection.insert(option);
option->SetSelectedState(false);
}
}
bool has_new_selection = false;
for (int list_index : list_indices) {
if (auto* option = OptionAtListIndex(list_index)) {
option->SetSelectedState(true);
option->SetDirty(true);
auto iter = old_selection.find(option);
if (iter != old_selection.end())
old_selection.erase(iter);
else
has_new_selection = true;
}
}
select_type_->UpdateTextStyleAndContent();
SetNeedsValidityCheck();
if (has_new_selection || !old_selection.IsEmpty()) {
DispatchInputEvent();
DispatchChangeEvent();
}
}
unsigned HTMLSelectElement::ListBoxSize() const {
DCHECK(!UsesMenuList());
const unsigned specified_size = size();
if (specified_size >= 1)
return specified_size;
return kDefaultListBoxSize;
}
void HTMLSelectElement::UpdateUsesMenuList() {
if (LayoutTheme::GetTheme().DelegatesMenuListRendering())
uses_menu_list_ = true;
else
uses_menu_list_ = !is_multiple_ && size_ <= 1;
}
int HTMLSelectElement::ActiveSelectionEndListIndex() const {
HTMLOptionElement* option = ActiveSelectionEnd();
return option ? option->ListIndex() : -1;
}
HTMLOptionElement* HTMLSelectElement::ActiveSelectionEnd() const {
return select_type_->ActiveSelectionEnd();
}
void HTMLSelectElement::add(
const HTMLOptionElementOrHTMLOptGroupElement& element,
const HTMLElementOrLong& before,
ExceptionState& exception_state) {
HTMLElement* element_to_insert;
DCHECK(!element.IsNull());
if (element.IsHTMLOptionElement())
element_to_insert = element.GetAsHTMLOptionElement();
else
element_to_insert = element.GetAsHTMLOptGroupElement();
HTMLElement* before_element;
if (before.IsHTMLElement())
before_element = before.GetAsHTMLElement();
else if (before.IsLong())
before_element = options()->item(before.GetAsLong());
else
before_element = nullptr;
InsertBefore(element_to_insert, before_element, exception_state);
SetNeedsValidityCheck();
}
void HTMLSelectElement::remove(int option_index) {
if (HTMLOptionElement* option = item(option_index))
option->remove(IGNORE_EXCEPTION_FOR_TESTING);
}
String HTMLSelectElement::value() const {
if (HTMLOptionElement* option = SelectedOption())
return option->value();
return "";
}
void HTMLSelectElement::setValue(const String& value, bool send_events) {
HTMLOptionElement* option = nullptr;
// Find the option with value() matching the given parameter and make it the
// current selection.
for (auto* const item : GetOptionList()) {
if (item->value() == value) {
option = item;
break;
}
}
HTMLOptionElement* previous_selected_option = SelectedOption();
SetSuggestedOption(nullptr);
if (is_autofilled_by_preview_)
SetAutofillState(WebAutofillState::kNotFilled);
SelectOptionFlags flags = kDeselectOtherOptionsFlag | kMakeOptionDirtyFlag;
if (send_events)
flags |= kDispatchInputAndChangeEventFlag;
SelectOption(option, flags);
if (send_events && previous_selected_option != option)
select_type_->ListBoxOnChange();
}
String HTMLSelectElement::SuggestedValue() const {
return suggested_option_ ? suggested_option_->value() : "";
}
void HTMLSelectElement::SetSuggestedValue(const String& value) {
if (value.IsNull()) {
SetSuggestedOption(nullptr);
return;
}
for (auto* const option : GetOptionList()) {
if (option->value() == value) {
SetSuggestedOption(option);
is_autofilled_by_preview_ = true;
return;
}
}
SetSuggestedOption(nullptr);
}
bool HTMLSelectElement::IsPresentationAttribute(
const QualifiedName& name) const {
if (name == html_names::kAlignAttr) {
// Don't map 'align' attribute. This matches what Firefox, Opera and IE do.
// See http://bugs.webkit.org/show_bug.cgi?id=12072
return false;
}
return HTMLFormControlElementWithState::IsPresentationAttribute(name);
}
void HTMLSelectElement::ParseAttribute(
const AttributeModificationParams& params) {
if (params.name == html_names::kSizeAttr) {
unsigned old_size = size_;
if (!ParseHTMLNonNegativeInteger(params.new_value, size_))
size_ = 0;
SetNeedsValidityCheck();
if (size_ != old_size) {
ChangeRendering();
UpdateUserAgentShadowTree(*UserAgentShadowRoot());
ResetToDefaultSelection();
select_type_->UpdateTextStyleAndContent();
select_type_->SaveListboxActiveSelection();
}
} else if (params.name == html_names::kMultipleAttr) {
ParseMultipleAttribute(params.new_value);
} else if (params.name == html_names::kAccesskeyAttr) {
// FIXME: ignore for the moment.
//
} else {
HTMLFormControlElementWithState::ParseAttribute(params);
}
}
bool HTMLSelectElement::MayTriggerVirtualKeyboard() const {
return true;
}
bool HTMLSelectElement::ShouldHaveFocusAppearance() const {
// For FormControlsRefresh don't draw focus ring for a select that has its
// popup open.
if (::features::IsFormControlsRefreshEnabled() && PopupIsVisible())
return false;
return HTMLFormControlElementWithState::ShouldHaveFocusAppearance();
}
bool HTMLSelectElement::CanSelectAll() const {
return !UsesMenuList();
}
LayoutObject* HTMLSelectElement::CreateLayoutObject(
const ComputedStyle& style,
LegacyLayout legacy_layout) {
if (UsesMenuList())
return LayoutObjectFactory::CreateFlexibleBox(*this, style, legacy_layout);
return LayoutObjectFactory::CreateBlockFlow(*this, style, legacy_layout);
}
HTMLCollection* HTMLSelectElement::selectedOptions() {
return EnsureCachedCollection<HTMLCollection>(kSelectedOptions);
}
HTMLOptionsCollection* HTMLSelectElement::options() {
return EnsureCachedCollection<HTMLOptionsCollection>(kSelectOptions);
}
void HTMLSelectElement::OptionElementChildrenChanged(
const HTMLOptionElement& option) {
SetNeedsValidityCheck();
if (option.Selected())
select_type_->UpdateTextStyleAndContent();
if (GetLayoutObject()) {
if (AXObjectCache* cache =
GetLayoutObject()->GetDocument().ExistingAXObjectCache())
cache->ChildrenChanged(this);
}
}
void HTMLSelectElement::AccessKeyAction(
SimulatedClickCreationScope creation_scope) {
focus();
DispatchSimulatedClick(nullptr, creation_scope);
}
Element* HTMLSelectElement::namedItem(const AtomicString& name) {
return options()->namedItem(name);
}
HTMLOptionElement* HTMLSelectElement::item(unsigned index) {
return options()->item(index);
}
void HTMLSelectElement::SetOption(unsigned index,
HTMLOptionElement* option,
ExceptionState& exception_state) {
int diff = index - length();
// We should check |index >= maxListItems| first to avoid integer overflow.
if (index >= kMaxListItems ||
GetListItems().size() + diff + 1 > kMaxListItems) {
GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning,
String::Format("Blocked to expand the option list and set an option at "
"index=%u. The maximum list length is %u.",
index, kMaxListItems)));
return;
}
HTMLOptionElementOrHTMLOptGroupElement element;
element.SetHTMLOptionElement(option);
HTMLElementOrLong before;
// Out of array bounds? First insert empty dummies.
if (diff > 0) {
setLength(index, exception_state);
// Replace an existing entry?
} else if (diff < 0) {
before.SetHTMLElement(options()->item(index + 1));
remove(index);
}
if (exception_state.HadException())
return;
// Finally add the new element.
EventQueueScope scope;
add(element, before, exception_state);
if (exception_state.HadException())
return;
if (diff >= 0 && option->Selected())
OptionSelectionStateChanged(option, true);
}
void HTMLSelectElement::setLength(unsigned new_len,
ExceptionState& exception_state) {
// We should check |newLen > maxListItems| first to avoid integer overflow.
if (new_len > kMaxListItems ||
GetListItems().size() + new_len - length() > kMaxListItems) {
GetDocument().AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning,
String::Format("Blocked to expand the option list to %u items. The "
"maximum list length is %u.",
new_len, kMaxListItems)));
return;
}
int diff = length() - new_len;
if (diff < 0) { // Add dummy elements.
do {
AppendChild(MakeGarbageCollected<HTMLOptionElement>(GetDocument()),
exception_state);
if (exception_state.HadException())
break;
} while (++diff);
} else {
// Removing children fires mutation events, which might mutate the DOM
// further, so we first copy out a list of elements that we intend to
// remove then attempt to remove them one at a time.
HeapVector<Member<HTMLOptionElement>> items_to_remove;
size_t option_index = 0;
for (auto* const option : GetOptionList()) {
if (option_index++ >= new_len) {
DCHECK(option->parentNode());
items_to_remove.push_back(option);
}
}
for (auto& item : items_to_remove) {
if (item->parentNode())
item->parentNode()->RemoveChild(item.Get(), exception_state);
}
}
SetNeedsValidityCheck();
}
bool HTMLSelectElement::IsRequiredFormControl() const {
return IsRequired();
}
HTMLOptionElement* HTMLSelectElement::OptionAtListIndex(int list_index) const {
if (list_index < 0)
return nullptr;
const ListItems& items = GetListItems();
if (static_cast<wtf_size_t>(list_index) >= items.size())
return nullptr;
return DynamicTo<HTMLOptionElement>(items[list_index].Get());
}
void HTMLSelectElement::SelectAll() {
select_type_->SelectAll();
}
const HTMLSelectElement::ListItems& HTMLSelectElement::GetListItems() const {
if (should_recalc_list_items_) {
RecalcListItems();
} else {
#if DCHECK_IS_ON()
HeapVector<Member<HTMLElement>> items = list_items_;
RecalcListItems();
DCHECK(items == list_items_);
#endif
}
return list_items_;
}
void HTMLSelectElement::InvalidateSelectedItems() {
if (HTMLCollection* collection =
CachedCollection<HTMLCollection>(kSelectedOptions))
collection->InvalidateCache();
}
void HTMLSelectElement::SetRecalcListItems() {
// FIXME: This function does a bunch of confusing things depending on if it
// is in the document or not.
should_recalc_list_items_ = true;
select_type_->MaximumOptionWidthMightBeChanged();
if (!isConnected()) {
if (HTMLOptionsCollection* collection =
CachedCollection<HTMLOptionsCollection>(kSelectOptions))
collection->InvalidateCache();
InvalidateSelectedItems();
}
if (GetLayoutObject()) {
if (AXObjectCache* cache =
GetLayoutObject()->GetDocument().ExistingAXObjectCache())
cache->ChildrenChanged(this);
}
}
void HTMLSelectElement::RecalcListItems() const {
TRACE_EVENT0("blink", "HTMLSelectElement::recalcListItems");
list_items_.resize(0);
should_recalc_list_items_ = false;
for (Element* current_element = ElementTraversal::FirstWithin(*this);
current_element && list_items_.size() < kMaxListItems;) {
auto* current_html_element = DynamicTo<HTMLElement>(current_element);
if (!current_html_element) {
current_element =
ElementTraversal::NextSkippingChildren(*current_element, this);
continue;
}
// We should ignore nested optgroup elements. The HTML parser flatten
// them. However we need to ignore nested optgroups built by DOM APIs.
// This behavior matches to IE and Firefox.
if (IsA<HTMLOptGroupElement>(*current_html_element)) {
if (current_html_element->parentNode() != this) {
current_element =
ElementTraversal::NextSkippingChildren(*current_html_element, this);
continue;
}
list_items_.push_back(current_html_element);
if (Element* next_element =
ElementTraversal::FirstWithin(*current_html_element)) {
current_element = next_element;
continue;
}
}
if (IsA<HTMLOptionElement>(*current_html_element))
list_items_.push_back(current_html_element);
if (IsA<HTMLHRElement>(*current_html_element))
list_items_.push_back(current_html_element);
// In conforming HTML code, only <optgroup> and <option> will be found
// within a <select>. We call NodeTraversal::nextSkippingChildren so
// that we only step into those tags that we choose to. For web-compat,
// we should cope with the case where odd tags like a <div> have been
// added but we handle this because such tags have already been removed
// from the <select>'s subtree at this point.
current_element =
ElementTraversal::NextSkippingChildren(*current_element, this);
}
}
void HTMLSelectElement::ResetToDefaultSelection(ResetReason reason) {
// https://html.spec.whatwg.org/C/#ask-for-a-reset
if (IsMultiple())
return;
HTMLOptionElement* first_enabled_option = nullptr;
HTMLOptionElement* last_selected_option = nullptr;
bool did_change = false;
int option_index = 0;
// We can't use HTMLSelectElement::options here because this function is
// called in Node::insertedInto and Node::removedFrom before invalidating
// node collections.
for (auto* const option : GetOptionList()) {
if (option->Selected()) {
if (last_selected_option) {
last_selected_option->SetSelectedState(false);
did_change = true;
}
last_selected_option = option;
}
if (!first_enabled_option && !option->IsDisabledFormControl()) {
first_enabled_option = option;
if (reason == kResetReasonSelectedOptionRemoved) {
// There must be no selected OPTIONs.
break;
}
}
++option_index;
}
if (!last_selected_option && size_ <= 1 &&
(!first_enabled_option ||
(first_enabled_option && !first_enabled_option->Selected()))) {
SelectOption(first_enabled_option,
reason == kResetReasonSelectedOptionRemoved
? 0
: kDeselectOtherOptionsFlag);
last_selected_option = first_enabled_option;
did_change = true;
}
if (did_change)
SetNeedsValidityCheck();
last_on_change_option_ = last_selected_option;
}
HTMLOptionElement* HTMLSelectElement::SelectedOption() const {
for (auto* const option : GetOptionList()) {
if (option->Selected())
return option;
}
return nullptr;
}
int HTMLSelectElement::selectedIndex() const {
unsigned index = 0;
// Return the number of the first option selected.
for (auto* const option : GetOptionList()) {
if (option->Selected())
return index;
++index;
}
return -1;
}
void HTMLSelectElement::setSelectedIndex(int index) {
SelectOption(item(index), kDeselectOtherOptionsFlag | kMakeOptionDirtyFlag);
}
int HTMLSelectElement::SelectedListIndex() const {
int index = 0;
for (const auto& item : GetListItems()) {
auto* option_element = DynamicTo<HTMLOptionElement>(item.Get());
if (option_element && option_element->Selected())
return index;
++index;
}
return -1;
}
void HTMLSelectElement::SetSuggestedOption(HTMLOptionElement* option) {
if (suggested_option_ == option)
return;
suggested_option_ = option;
select_type_->DidSetSuggestedOption(option);
}
void HTMLSelectElement::OptionSelectionStateChanged(HTMLOptionElement* option,
bool option_is_selected) {
DCHECK_EQ(option->OwnerSelectElement(), this);
if (option_is_selected)
SelectOption(option, IsMultiple() ? 0 : kDeselectOtherOptionsFlag);
else if (!UsesMenuList() || IsMultiple())
SelectOption(nullptr, IsMultiple() ? 0 : kDeselectOtherOptionsFlag);
else
ResetToDefaultSelection();
}
void HTMLSelectElement::ChildrenChanged(const ChildrenChange& change) {
HTMLFormControlElementWithState::ChildrenChanged(change);
if (change.type == ChildrenChangeType::kElementInserted) {
if (auto* option = DynamicTo<HTMLOptionElement>(change.sibling_changed)) {
OptionInserted(*option, option->Selected());
} else if (auto* optgroup =
DynamicTo<HTMLOptGroupElement>(change.sibling_changed)) {
for (auto& child_option :
Traversal<HTMLOptionElement>::ChildrenOf(*optgroup))
OptionInserted(child_option, child_option.Selected());
}
} else if (change.type == ChildrenChangeType::kElementRemoved) {
if (auto* option = DynamicTo<HTMLOptionElement>(change.sibling_changed)) {
OptionRemoved(*option);
} else if (auto* optgroup =
DynamicTo<HTMLOptGroupElement>(change.sibling_changed)) {
for (auto& child_option :
Traversal<HTMLOptionElement>::ChildrenOf(*optgroup))
OptionRemoved(child_option);
}
} else if (change.type == ChildrenChangeType::kAllChildrenRemoved) {
for (Node* node : change.removed_nodes) {
if (auto* option = DynamicTo<HTMLOptionElement>(node)) {
OptionRemoved(*option);
} else if (auto* optgroup = DynamicTo<HTMLOptGroupElement>(node)) {
for (auto& child_option :
Traversal<HTMLOptionElement>::ChildrenOf(*optgroup))
OptionRemoved(child_option);
}
}
}
}
bool HTMLSelectElement::ChildrenChangedAllChildrenRemovedNeedsList() const {
return true;
}
void HTMLSelectElement::OptionInserted(HTMLOptionElement& option,
bool option_is_selected) {
DCHECK_EQ(option.OwnerSelectElement(), this);
option.SetWasOptionInsertedCalled(true);
SetRecalcListItems();
if (option_is_selected) {
SelectOption(&option, IsMultiple() ? 0 : kDeselectOtherOptionsFlag);
} else {
// No need to reset if we already have a selected option.
if (!last_on_change_option_)
ResetToDefaultSelection();
}
SetNeedsValidityCheck();
select_type_->ClearLastOnChangeSelection();
if (!GetDocument().IsActive())
return;
GetDocument()
.GetFrame()
->GetPage()
->GetChromeClient()
.SelectFieldOptionsChanged(*this);
}
void HTMLSelectElement::OptionRemoved(HTMLOptionElement& option) {
option.SetWasOptionInsertedCalled(false);
SetRecalcListItems();
if (option.Selected())
ResetToDefaultSelection(kResetReasonSelectedOptionRemoved);
else if (!last_on_change_option_)
ResetToDefaultSelection();
if (last_on_change_option_ == &option)
last_on_change_option_.Clear();
select_type_->OptionRemoved(option);
if (suggested_option_ == &option)
SetSuggestedOption(nullptr);
if (option.Selected())
SetAutofillState(WebAutofillState::kNotFilled);
SetNeedsValidityCheck();
select_type_->ClearLastOnChangeSelection();
if (!GetDocument().IsActive())
return;
GetDocument()
.GetFrame()
->GetPage()
->GetChromeClient()
.SelectFieldOptionsChanged(*this);
}
void HTMLSelectElement::OptGroupInsertedOrRemoved(
HTMLOptGroupElement& optgroup) {
SetRecalcListItems();
SetNeedsValidityCheck();
select_type_->ClearLastOnChangeSelection();
}
void HTMLSelectElement::HrInsertedOrRemoved(HTMLHRElement& hr) {
SetRecalcListItems();
select_type_->ClearLastOnChangeSelection();
}
// TODO(tkent): This function is not efficient. It contains multiple O(N)
// operations. crbug.com/577989.
void HTMLSelectElement::SelectOption(HTMLOptionElement* element,
SelectOptionFlags flags) {
TRACE_EVENT0("blink", "HTMLSelectElement::selectOption");
bool should_update_popup = false;
// SelectedOption() is O(N).
if (IsAutofilled() && SelectedOption() != element)
SetAutofillState(WebAutofillState::kNotFilled);
if (element) {
if (!element->Selected())
should_update_popup = true;
element->SetSelectedState(true);
if (flags & kMakeOptionDirtyFlag)
element->SetDirty(true);
}
// DeselectItemsWithoutValidation() is O(N).
if (flags & kDeselectOtherOptionsFlag)
should_update_popup |= DeselectItemsWithoutValidation(element);
select_type_->DidSelectOption(element, flags, should_update_popup);
NotifyFormStateChanged();
if (LocalFrame::HasTransientUserActivation(GetDocument().GetFrame()) &&
GetDocument().IsActive()) {
GetDocument()
.GetPage()
->GetChromeClient()
.DidChangeSelectionInSelectControl(*this);
}
}
void HTMLSelectElement::DispatchFocusEvent(
Element* old_focused_element,
mojom::blink::FocusType type,
InputDeviceCapabilities* source_capabilities) {
// Save the selection so it can be compared to the new selection when
// dispatching change events during blur event dispatch.
if (UsesMenuList())
select_type_->SaveLastSelection();
HTMLFormControlElementWithState::DispatchFocusEvent(old_focused_element, type,
source_capabilities);
}
void HTMLSelectElement::DispatchBlurEvent(
Element* new_focused_element,
mojom::blink::FocusType type,
InputDeviceCapabilities* source_capabilities) {
type_ahead_.ResetSession();
select_type_->DidBlur();
HTMLFormControlElementWithState::DispatchBlurEvent(new_focused_element, type,
source_capabilities);
}
// Returns true if selection state of any OPTIONs is changed.
bool HTMLSelectElement::DeselectItemsWithoutValidation(
HTMLOptionElement* exclude_element) {
if (!IsMultiple() && UsesMenuList() && last_on_change_option_ &&
last_on_change_option_ != exclude_element) {
last_on_change_option_->SetSelectedState(false);
return true;
}
bool did_update_selection = false;
for (auto* const option : GetOptionList()) {
if (option == exclude_element)
continue;
if (!option->WasOptionInsertedCalled())
continue;
if (option->Selected())
did_update_selection = true;
option->SetSelectedState(false);
}
return did_update_selection;
}
FormControlState HTMLSelectElement::SaveFormControlState() const {
const ListItems& items = GetListItems();
wtf_size_t length = items.size();
FormControlState state;
for (wtf_size_t i = 0; i < length; ++i) {
auto* option = DynamicTo<HTMLOptionElement>(items[i].Get());
if (!option || !option->Selected())
continue;
state.Append(option->value());
state.Append(String::Number(i));
if (!IsMultiple())
break;
}
return state;
}
wtf_size_t HTMLSelectElement::SearchOptionsForValue(
const String& value,
wtf_size_t list_index_start,
wtf_size_t list_index_end) const {
const ListItems& items = GetListItems();
wtf_size_t loop_end_index = std::min(items.size(), list_index_end);
for (wtf_size_t i = list_index_start; i < loop_end_index; ++i) {
auto* option_element = DynamicTo<HTMLOptionElement>(items[i].Get());
if (!option_element)
continue;
if (option_element->value() == value)
return i;
}
return kNotFound;
}
void HTMLSelectElement::RestoreFormControlState(const FormControlState& state) {
RecalcListItems();
const ListItems& items = GetListItems();
wtf_size_t items_size = items.size();
if (items_size == 0)
return;
SelectOption(nullptr, kDeselectOtherOptionsFlag);
// The saved state should have at least one value and an index.
DCHECK_GE(state.ValueSize(), 2u);
if (!IsMultiple()) {
unsigned index = state[1].ToUInt();
auto* option_element =
index < items_size ? DynamicTo<HTMLOptionElement>(items[index].Get())
: nullptr;
if (option_element && option_element->value() == state[0]) {
option_element->SetSelectedState(true);
option_element->SetDirty(true);
last_on_change_option_ = option_element;
} else {
wtf_size_t found_index = SearchOptionsForValue(state[0], 0, items_size);
if (found_index != kNotFound) {
auto* found_option_element =
To<HTMLOptionElement>(items[found_index].Get());
found_option_element->SetSelectedState(true);
found_option_element->SetDirty(true);
last_on_change_option_ = found_option_element;
}
}
} else {
wtf_size_t start_index = 0;
for (wtf_size_t i = 0; i < state.ValueSize(); i += 2) {
const String& value = state[i];
const unsigned index = state[i + 1].ToUInt();
auto* option_element =
index < items_size ? DynamicTo<HTMLOptionElement>(items[index].Get())
: nullptr;
if (option_element && option_element->value() == value) {
option_element->SetSelectedState(true);
option_element->SetDirty(true);
start_index = index + 1;
} else {
wtf_size_t found_index =
SearchOptionsForValue(value, start_index, items_size);
if (found_index == kNotFound)
found_index = SearchOptionsForValue(value, 0, start_index);
if (found_index == kNotFound)
continue;
auto* found_option_element =
To<HTMLOptionElement>(items[found_index].Get());
found_option_element->SetSelectedState(true);
found_option_element->SetDirty(true);
start_index = found_index + 1;
}
}
}
SetNeedsValidityCheck();
select_type_->UpdateTextStyleAndContent();
}
void HTMLSelectElement::ParseMultipleAttribute(const AtomicString& value) {
bool old_multiple = is_multiple_;
HTMLOptionElement* old_selected_option = SelectedOption();
is_multiple_ = !value.IsNull();
SetNeedsValidityCheck();
ChangeRendering();
UpdateUserAgentShadowTree(*UserAgentShadowRoot());
// Restore selectedIndex after changing the multiple flag to preserve
// selection as single-line and multi-line has different defaults.
if (old_multiple != is_multiple_) {
// Preserving the first selection is compatible with Firefox and
// WebKit. However Edge seems to "ask for a reset" simply. As of 2016
// March, the HTML specification says nothing about this.
if (old_selected_option)
SelectOption(old_selected_option, kDeselectOtherOptionsFlag);
else
ResetToDefaultSelection();
}
select_type_->UpdateTextStyleAndContent();
}
void HTMLSelectElement::AppendToFormData(FormData& form_data) {
const AtomicString& name = GetName();
if (name.IsEmpty())
return;
for (auto* const option : GetOptionList()) {
if (option->Selected() && !option->IsDisabledFormControl())
form_data.AppendFromElement(name, option->value());
}
}
void HTMLSelectElement::ResetImpl() {
for (auto* const option : GetOptionList()) {
option->SetSelectedState(
option->FastHasAttribute(html_names::kSelectedAttr));
option->SetDirty(false);
}
ResetToDefaultSelection();
select_type_->UpdateTextStyleAndContent();
SetNeedsValidityCheck();
}
bool HTMLSelectElement::PopupIsVisible() const {
return select_type_->PopupIsVisible();
}
int HTMLSelectElement::ListIndexForOption(const HTMLOptionElement& option) {
const ListItems& items = GetListItems();
wtf_size_t length = items.size();
for (wtf_size_t i = 0; i < length; ++i) {
if (items[i].Get() == &option)
return i;
}
return -1;
}
AutoscrollController* HTMLSelectElement::GetAutoscrollController() const {
if (Page* page = GetDocument().GetPage())
return &page->GetAutoscrollController();
return nullptr;
}
LayoutBox* HTMLSelectElement::AutoscrollBox() {
return !UsesMenuList() ? GetLayoutBox() : nullptr;
}
void HTMLSelectElement::StopAutoscroll() {
if (!IsDisabledFormControl())
select_type_->HandleMouseRelease();
}
void HTMLSelectElement::DefaultEventHandler(Event& event) {
if (!GetLayoutObject())
return;
if (event.type() == event_type_names::kClick ||
event.type() == event_type_names::kChange) {
user_has_edited_the_field_ = true;
}
if (IsDisabledFormControl()) {
HTMLFormControlElementWithState::DefaultEventHandler(event);
return;
}
if (select_type_->DefaultEventHandler(event)) {
event.SetDefaultHandled();
return;
}
auto* keyboard_event = DynamicTo<KeyboardEvent>(event);
if (event.type() == event_type_names::kKeypress && keyboard_event) {
if (!keyboard_event->ctrlKey() && !keyboard_event->altKey() &&
!keyboard_event->metaKey() &&
WTF::unicode::IsPrintableChar(keyboard_event->charCode())) {
TypeAheadFind(*keyboard_event);
event.SetDefaultHandled();
return;
}
}
HTMLFormControlElementWithState::DefaultEventHandler(event);
}
HTMLOptionElement* HTMLSelectElement::LastSelectedOption() const {
const ListItems& items = GetListItems();
for (wtf_size_t i = items.size(); i;) {
if (HTMLOptionElement* option = OptionAtListIndex(--i)) {
if (option->Selected())
return option;
}
}
return nullptr;
}
int HTMLSelectElement::IndexOfSelectedOption() const {
return SelectedListIndex();
}
int HTMLSelectElement::OptionCount() const {
return GetListItems().size();
}
String HTMLSelectElement::OptionAtIndex(int index) const {
if (HTMLOptionElement* option = OptionAtListIndex(index)) {
if (!option->IsDisabledFormControl())
return option->DisplayLabel();
}
return String();
}
void HTMLSelectElement::TypeAheadFind(const KeyboardEvent& event) {
int index = type_ahead_.HandleEvent(
event, TypeAhead::kMatchPrefix | TypeAhead::kCycleFirstChar);
if (index < 0)
return;
SelectOption(OptionAtListIndex(index), kDeselectOtherOptionsFlag |
kMakeOptionDirtyFlag |
kDispatchInputAndChangeEventFlag);
select_type_->ListBoxOnChange();
}
void HTMLSelectElement::SelectOptionByAccessKey(HTMLOptionElement* option) {
// First bring into focus the list box.
if (!IsFocused())
AccessKeyAction(SimulatedClickCreationScope::kFromUserAgent);
if (!option || option->OwnerSelectElement() != this)
return;
EventQueueScope scope;
// If this index is already selected, unselect. otherwise update the
// selected index.
SelectOptionFlags flags = kDispatchInputAndChangeEventFlag |
(IsMultiple() ? 0 : kDeselectOtherOptionsFlag);
if (option->Selected()) {
if (UsesMenuList())
SelectOption(nullptr, flags);
else
option->SetSelectedState(false);
} else {
SelectOption(option, flags);
}
option->SetDirty(true);
select_type_->ListBoxOnChange();
select_type_->ScrollToSelection();
}
unsigned HTMLSelectElement::length() const {
unsigned options = 0;
for (auto* const option : GetOptionList()) {
ALLOW_UNUSED_LOCAL(option);
++options;
}
return options;
}
void HTMLSelectElement::FinishParsingChildren() {
HTMLFormControlElementWithState::FinishParsingChildren();
if (UsesMenuList())
return;
select_type_->ScrollToOption(SelectedOption());
if (AXObjectCache* cache = GetDocument().ExistingAXObjectCache())
cache->ListboxActiveIndexChanged(this);
}
IndexedPropertySetterResult HTMLSelectElement::AnonymousIndexedSetter(
unsigned index,
HTMLOptionElement* value,
ExceptionState& exception_state) {
if (!value) { // undefined or null
remove(index);
return IndexedPropertySetterResult::kIntercepted;
}
SetOption(index, value, exception_state);
return IndexedPropertySetterResult::kIntercepted;
}
bool HTMLSelectElement::IsInteractiveContent() const {
return true;
}
void HTMLSelectElement::Trace(Visitor* visitor) const {
visitor->Trace(list_items_);
visitor->Trace(last_on_change_option_);
visitor->Trace(suggested_option_);
visitor->Trace(select_type_);
HTMLFormControlElementWithState::Trace(visitor);
}
void HTMLSelectElement::DidAddUserAgentShadowRoot(ShadowRoot& root) {
// Even if UsesMenuList(), the <slot> is necessary to have ComputedStyles
// for <option>s. LayoutFlexibleBox::IsChildAllowed() rejects all of
// LayoutObject children except for MenuListInnerElement's.
root.AppendChild(
HTMLSlotElement::CreateUserAgentCustomAssignSlot(GetDocument()));
UpdateUserAgentShadowTree(root);
select_type_->UpdateTextStyleAndContent();
}
void HTMLSelectElement::UpdateUserAgentShadowTree(ShadowRoot& root) {
// Remove all children of the ShadowRoot except for <slot>.
Node* node = root.firstChild();
while (node) {
if (IsA<HTMLSlotElement>(node)) {
node = node->nextSibling();
} else {
auto* will_be_removed = node;
node = node->nextSibling();
will_be_removed->remove();
}
}
select_type_->CreateShadowSubtree(root);
}
Element& HTMLSelectElement::InnerElement() const {
return select_type_->InnerElement();
}
AXObject* HTMLSelectElement::PopupRootAXObject() const {
return select_type_->PopupRootAXObject();
}
HTMLOptionElement* HTMLSelectElement::SpatialNavigationFocusedOption() {
return select_type_->SpatialNavigationFocusedOption();
}
String HTMLSelectElement::ItemText(const Element& element) const {
String item_string;
if (auto* optgroup = DynamicTo<HTMLOptGroupElement>(element))
item_string = optgroup->GroupLabelText();
else if (auto* option = DynamicTo<HTMLOptionElement>(element))
item_string = option->TextIndentedToRespectGroupLabel();
if (GetLayoutObject() && GetLayoutObject()->Style())
GetLayoutObject()->Style()->ApplyTextTransform(&item_string);
return item_string;
}
bool HTMLSelectElement::ItemIsDisplayNone(Element& element) const {
if (auto* option = DynamicTo<HTMLOptionElement>(element))
return option->IsDisplayNone();
const ComputedStyle* style = ItemComputedStyle(element);
return !style || style->Display() == EDisplay::kNone;
}
const ComputedStyle* HTMLSelectElement::ItemComputedStyle(
Element& element) const {
return element.GetComputedStyle() ? element.GetComputedStyle()
: element.EnsureComputedStyle();
}
LayoutUnit HTMLSelectElement::ClientPaddingLeft() const {
DCHECK(UsesMenuList());
auto* this_box = GetLayoutBox();
if (!this_box || !InnerElement().GetLayoutBox())
return LayoutUnit();
LayoutTheme& theme = LayoutTheme::GetTheme();
const ComputedStyle& style = this_box->StyleRef();
int inner_padding =
style.IsLeftToRightDirection()
? theme.PopupInternalPaddingStart(style)
: theme.PopupInternalPaddingEnd(GetDocument().GetFrame(), style);
return this_box->PaddingLeft() + inner_padding;
}
LayoutUnit HTMLSelectElement::ClientPaddingRight() const {
DCHECK(UsesMenuList());
auto* this_box = GetLayoutBox();
if (!this_box || !InnerElement().GetLayoutBox())
return LayoutUnit();
LayoutTheme& theme = LayoutTheme::GetTheme();
const ComputedStyle& style = this_box->StyleRef();
int inner_padding =
style.IsLeftToRightDirection()
? theme.PopupInternalPaddingEnd(GetDocument().GetFrame(), style)
: theme.PopupInternalPaddingStart(style);
return this_box->PaddingRight() + inner_padding;
}
void HTMLSelectElement::PopupDidHide() {
select_type_->PopupDidHide();
}
void HTMLSelectElement::SetIndexToSelectOnCancel(int list_index) {
index_to_select_on_cancel_ = list_index;
select_type_->UpdateTextStyleAndContent();
}
HTMLOptionElement* HTMLSelectElement::OptionToBeShownForTesting() const {
return select_type_->OptionToBeShown();
}
void HTMLSelectElement::SelectOptionByPopup(int list_index) {
DCHECK(UsesMenuList());
// Check to ensure a page navigation has not occurred while the popup was
// up.
Document& doc = GetDocument();
if (&doc != doc.GetFrame()->GetDocument())
return;
SetIndexToSelectOnCancel(-1);
HTMLOptionElement* option = OptionAtListIndex(list_index);
// Bail out if this index is already the selected one, to avoid running
// unnecessary JavaScript that can mess up autofill when there is no actual
// change (see https://bugs.webkit.org/show_bug.cgi?id=35256 and
// <rdar://7467917>). The selectOption function does not behave this way,
// possibly because other callers need a change event even in cases where
// the selected option is not change.
if (option == SelectedOption())
return;
SelectOption(option, kDeselectOtherOptionsFlag | kMakeOptionDirtyFlag |
kDispatchInputAndChangeEventFlag);
}
void HTMLSelectElement::PopupDidCancel() {
if (index_to_select_on_cancel_ >= 0)
SelectOptionByPopup(index_to_select_on_cancel_);
}
void HTMLSelectElement::ProvisionalSelectionChanged(unsigned list_index) {
SetIndexToSelectOnCancel(list_index);
}
void HTMLSelectElement::ShowPopup() {
select_type_->ShowPopup();
}
void HTMLSelectElement::HidePopup() {
select_type_->HidePopup();
}
PopupMenu* HTMLSelectElement::PopupForTesting() const {
return select_type_->PopupForTesting();
}
void HTMLSelectElement::DidRecalcStyle(const StyleRecalcChange change) {
HTMLFormControlElementWithState::DidRecalcStyle(change);
select_type_->DidRecalcStyle(change);
}
void HTMLSelectElement::AttachLayoutTree(AttachContext& context) {
HTMLFormControlElementWithState::AttachLayoutTree(context);
// The call to UpdateTextStyle() needs to go after the call through
// to the base class's AttachLayoutTree() because that can sometimes do a
// close on the LayoutObject.
select_type_->UpdateTextStyle();
if (const ComputedStyle* style = GetComputedStyle()) {
if (style->Visibility() != EVisibility::kHidden) {
if (IsMultiple())
UseCounter::Count(GetDocument(), WebFeature::kSelectElementMultiple);
else
UseCounter::Count(GetDocument(), WebFeature::kSelectElementSingle);
}
}
}
void HTMLSelectElement::DetachLayoutTree(bool performing_reattach) {
HTMLFormControlElementWithState::DetachLayoutTree(performing_reattach);
select_type_->DidDetachLayoutTree();
}
void HTMLSelectElement::ResetTypeAheadSessionForTesting() {
type_ahead_.ResetSession();
}
void HTMLSelectElement::CloneNonAttributePropertiesFrom(
const Element& source,
CloneChildrenFlag flag) {
const auto& source_element = static_cast<const HTMLSelectElement&>(source);
user_has_edited_the_field_ = source_element.user_has_edited_the_field_;
HTMLFormControlElement::CloneNonAttributePropertiesFrom(source, flag);
}
void HTMLSelectElement::ChangeRendering() {
select_type_->DidDetachLayoutTree();
bool old_uses_menu_list = UsesMenuList();
UpdateUsesMenuList();
if (UsesMenuList() != old_uses_menu_list) {
select_type_->WillBeDestroyed();
select_type_ = SelectType::Create(*this);
}
if (!InActiveDocument())
return;
// TODO(futhark): SetForceReattachLayoutTree() should be the correct way to
// create a new layout tree, but the code for updating the selected index
// relies on the layout tree to be nuked.
DetachLayoutTree();
SetNeedsStyleRecalc(kLocalStyleChange, StyleChangeReasonForTracing::Create(
style_change_reason::kControl));
}
const ComputedStyle* HTMLSelectElement::OptionStyle() const {
return select_type_->OptionStyle();
}
} // namespace blink