summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/core/html/forms/select_type.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/blink/renderer/core/html/forms/select_type.cc')
-rw-r--r--chromium/third_party/blink/renderer/core/html/forms/select_type.cc1252
1 files changed, 1252 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/html/forms/select_type.cc b/chromium/third_party/blink/renderer/core/html/forms/select_type.cc
new file mode 100644
index 00000000000..345cbeb2682
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/html/forms/select_type.cc
@@ -0,0 +1,1252 @@
+/*
+ * 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/select_type.h"
+
+#include "build/build_config.h"
+#include "third_party/blink/public/strings/grit/blink_strings.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_mutation_observer_init.h"
+#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
+#include "third_party/blink/renderer/core/dom/mutation_observer.h"
+#include "third_party/blink/renderer/core/dom/mutation_record.h"
+#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/events/gesture_event.h"
+#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/events/mouse_event.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
+#include "third_party/blink/renderer/core/html/forms/popup_menu.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/input/input_device_capabilities.h"
+#include "third_party/blink/renderer/core/layout/layout_box.h"
+#include "third_party/blink/renderer/core/page/autoscroll_controller.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/core/paint/paint_layer.h"
+#include "third_party/blink/renderer/platform/text/platform_locale.h"
+#include "ui/base/ui_base_features.h"
+
+namespace blink {
+
+class PopupUpdater;
+
+namespace {
+
+HTMLOptionElement* EventTargetOption(const Event& event) {
+ return DynamicTo<HTMLOptionElement>(event.target()->ToNode());
+}
+
+} // anonymous namespace
+
+class MenuListSelectType final : public SelectType {
+ public:
+ explicit MenuListSelectType(HTMLSelectElement& select) : SelectType(select) {}
+ void Trace(Visitor* visitor) override;
+
+ bool DefaultEventHandler(const Event& event) override;
+ void DidSelectOption(HTMLOptionElement* element,
+ HTMLSelectElement::SelectOptionFlags flags,
+ bool should_update_popup) override;
+ void DidBlur() override;
+ void DidDetachLayoutTree() override;
+ void DidRecalcStyle(const StyleRecalcChange change) override;
+ void DidSetSuggestedOption(HTMLOptionElement* option) override;
+ void SaveLastSelection() override;
+
+ void UpdateTextStyle() override { UpdateTextStyleInternal(); }
+ void UpdateTextStyleAndContent() override;
+ HTMLOptionElement* OptionToBeShown() const override;
+ const ComputedStyle* OptionStyle() const override {
+ return option_style_.get();
+ }
+ void MaximumOptionWidthMightBeChanged() const override;
+
+ void ShowPopup() override;
+ void HidePopup() override;
+ void PopupDidHide() override;
+ bool PopupIsVisible() const override;
+ PopupMenu* PopupForTesting() const override;
+
+ void DidMutateSubtree();
+
+ private:
+ bool ShouldOpenPopupForKeyDownEvent(const KeyboardEvent& event);
+ bool ShouldOpenPopupForKeyPressEvent(const KeyboardEvent& event);
+ // Returns true if this function handled the event.
+ bool HandlePopupOpenKeyboardEvent();
+ void SetPopupIsVisible(bool popup_is_visible);
+ void DispatchEventsIfSelectedOptionChanged();
+ String UpdateTextStyleInternal();
+ void DidUpdateActiveOption(HTMLOptionElement* option);
+ void ObserveTreeMutation();
+ void UnobserveTreeMutation();
+
+ Member<PopupMenu> popup_;
+ Member<PopupUpdater> popup_updater_;
+ scoped_refptr<const ComputedStyle> option_style_;
+ int ax_menulist_last_active_index_ = -1;
+ bool has_updated_menulist_active_option_ = false;
+ bool popup_is_visible_ = false;
+ bool snav_arrow_key_selection_ = false;
+};
+
+void MenuListSelectType::Trace(Visitor* visitor) {
+ visitor->Trace(popup_);
+ visitor->Trace(popup_updater_);
+ SelectType::Trace(visitor);
+}
+
+bool MenuListSelectType::DefaultEventHandler(const Event& event) {
+ // We need to make the layout tree up-to-date to have GetLayoutObject() give
+ // the correct result below. An author event handler may have set display to
+ // some element to none which will cause a layout tree detach.
+ select_->GetDocument().UpdateStyleAndLayoutTree();
+
+ const auto* key_event = DynamicTo<KeyboardEvent>(event);
+ if (event.type() == event_type_names::kKeydown) {
+ if (!select_->GetLayoutObject() || !key_event)
+ return false;
+
+ if (ShouldOpenPopupForKeyDownEvent(*key_event))
+ return HandlePopupOpenKeyboardEvent();
+
+ // When using spatial navigation, we want to be able to navigate away
+ // from the select element when the user hits any of the arrow keys,
+ // instead of changing the selection.
+ if (IsSpatialNavigationEnabled(select_->GetDocument().GetFrame())) {
+ if (!snav_arrow_key_selection_)
+ return false;
+ }
+
+ // The key handling below shouldn't be used for non spatial navigation
+ // mode Mac
+ if (LayoutTheme::GetTheme().PopsMenuByArrowKeys() &&
+ !IsSpatialNavigationEnabled(select_->GetDocument().GetFrame()))
+ return false;
+
+ int ignore_modifiers = WebInputEvent::kShiftKey |
+ WebInputEvent::kControlKey | WebInputEvent::kAltKey |
+ WebInputEvent::kMetaKey;
+ if (key_event->GetModifiers() & ignore_modifiers)
+ return false;
+
+ const String& key = key_event->key();
+ bool handled = true;
+ HTMLOptionElement* option = select_->SelectedOption();
+ int list_index = option ? option->ListIndex() : -1;
+
+ if (key == "ArrowDown" || key == "ArrowRight") {
+ option = NextValidOption(list_index, kSkipForwards, 1);
+ } else if (key == "ArrowUp" || key == "ArrowLeft") {
+ option = NextValidOption(list_index, kSkipBackwards, 1);
+ } else if (key == "PageDown") {
+ option = NextValidOption(list_index, kSkipForwards, 3);
+ } else if (key == "PageUp") {
+ option = NextValidOption(list_index, kSkipBackwards, 3);
+ } else if (key == "Home") {
+ option = FirstSelectableOption();
+ } else if (key == "End") {
+ option = LastSelectableOption();
+ } else {
+ handled = false;
+ }
+
+ if (handled && option) {
+ select_->SelectOption(
+ option, HTMLSelectElement::kDeselectOtherOptionsFlag |
+ HTMLSelectElement::kMakeOptionDirtyFlag |
+ HTMLSelectElement::kDispatchInputAndChangeEventFlag);
+ }
+ return handled;
+ }
+
+ if (event.type() == event_type_names::kKeypress) {
+ if (!select_->GetLayoutObject() || !key_event)
+ return false;
+
+ int key_code = key_event->keyCode();
+ if (key_code == ' ' &&
+ IsSpatialNavigationEnabled(select_->GetDocument().GetFrame())) {
+ // Use space to toggle arrow key handling for selection change or
+ // spatial navigation.
+ snav_arrow_key_selection_ = !snav_arrow_key_selection_;
+ return true;
+ }
+
+ if (ShouldOpenPopupForKeyPressEvent(*key_event))
+ return HandlePopupOpenKeyboardEvent();
+
+ if (!LayoutTheme::GetTheme().PopsMenuByReturnKey() && key_code == '\r') {
+ if (HTMLFormElement* form = select_->Form())
+ form->SubmitImplicitly(event, false);
+ DispatchEventsIfSelectedOptionChanged();
+ return true;
+ }
+ return false;
+ }
+
+ const auto* mouse_event = DynamicTo<MouseEvent>(event);
+ if (event.type() == event_type_names::kMousedown && mouse_event &&
+ mouse_event->button() ==
+ static_cast<int16_t>(WebPointerProperties::Button::kLeft)) {
+ InputDeviceCapabilities* source_capabilities =
+ select_->GetDocument()
+ .domWindow()
+ ->GetInputDeviceCapabilities()
+ ->FiresTouchEvents(mouse_event->FromTouch());
+ select_->focus(FocusParams(SelectionBehaviorOnFocus::kRestore,
+ mojom::blink::FocusType::kNone,
+ source_capabilities));
+ if (select_->GetLayoutObject() && !will_be_destroyed_ &&
+ !select_->IsDisabledFormControl()) {
+ if (PopupIsVisible()) {
+ HidePopup();
+ } else {
+ // Save the selection so it can be compared to the new selection
+ // when we call onChange during selectOption, which gets called
+ // from selectOptionByPopup, which gets called after the user
+ // makes a selection from the menu.
+ SaveLastSelection();
+ // TODO(lanwei): Will check if we need to add
+ // InputDeviceCapabilities here when select menu list gets
+ // focus, see https://crbug.com/476530.
+ ShowPopup();
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+bool MenuListSelectType::ShouldOpenPopupForKeyDownEvent(
+ const KeyboardEvent& event) {
+ const String& key = event.key();
+ LayoutTheme& layout_theme = LayoutTheme::GetTheme();
+
+ if (IsSpatialNavigationEnabled(select_->GetDocument().GetFrame()))
+ return false;
+
+ return ((layout_theme.PopsMenuByArrowKeys() &&
+ (key == "ArrowDown" || key == "ArrowUp")) ||
+ (layout_theme.PopsMenuByAltDownUpOrF4Key() &&
+ (key == "ArrowDown" || key == "ArrowUp") && event.altKey()) ||
+ (layout_theme.PopsMenuByAltDownUpOrF4Key() &&
+ (!event.altKey() && !event.ctrlKey() && key == "F4")));
+}
+
+bool MenuListSelectType::ShouldOpenPopupForKeyPressEvent(
+ const KeyboardEvent& event) {
+ LayoutTheme& layout_theme = LayoutTheme::GetTheme();
+ int key_code = event.keyCode();
+
+ return ((layout_theme.PopsMenuBySpaceKey() && key_code == ' ' &&
+ !select_->type_ahead_.HasActiveSession(event)) ||
+ (layout_theme.PopsMenuByReturnKey() && key_code == '\r'));
+}
+
+bool MenuListSelectType::HandlePopupOpenKeyboardEvent() {
+ select_->focus();
+ // Calling focus() may cause us to lose our LayoutObject. Return true so
+ // that our caller doesn't process the event further, but don't set
+ // the event as handled.
+ if (!select_->GetLayoutObject() || will_be_destroyed_ ||
+ select_->IsDisabledFormControl())
+ return false;
+ // Save the selection so it can be compared to the new selection when
+ // dispatching change events during SelectOption, which gets called from
+ // SelectOptionByPopup, which gets called after the user makes a selection
+ // from the menu.
+ SaveLastSelection();
+ ShowPopup();
+ return true;
+}
+
+void MenuListSelectType::ShowPopup() {
+ if (PopupIsVisible())
+ return;
+ Document& document = select_->GetDocument();
+ if (document.GetPage()->GetChromeClient().HasOpenedPopup())
+ return;
+ if (!select_->GetLayoutObject())
+ return;
+ if (select_->VisibleBoundsInVisualViewport().IsEmpty())
+ return;
+
+ if (!popup_) {
+ popup_ = document.GetPage()->GetChromeClient().OpenPopupMenu(
+ *document.GetFrame(), *select_);
+ }
+ if (!popup_)
+ return;
+
+ SetPopupIsVisible(true);
+ ObserveTreeMutation();
+
+ popup_->Show();
+ if (AXObjectCache* cache = document.ExistingAXObjectCache())
+ cache->DidShowMenuListPopup(select_->GetLayoutObject());
+}
+
+void MenuListSelectType::HidePopup() {
+ if (popup_)
+ popup_->Hide();
+}
+
+void MenuListSelectType::PopupDidHide() {
+ SetPopupIsVisible(false);
+ UnobserveTreeMutation();
+ if (AXObjectCache* cache = select_->GetDocument().ExistingAXObjectCache()) {
+ if (auto* layout_object = select_->GetLayoutObject())
+ cache->DidHideMenuListPopup(layout_object);
+ }
+}
+
+bool MenuListSelectType::PopupIsVisible() const {
+ return popup_is_visible_;
+}
+
+void MenuListSelectType::SetPopupIsVisible(bool popup_is_visible) {
+ popup_is_visible_ = popup_is_visible;
+ if (!::features::IsFormControlsRefreshEnabled())
+ return;
+ if (auto* layout_object = select_->GetLayoutObject()) {
+ // Invalidate paint to ensure that the focus ring is updated.
+ layout_object->SetShouldDoFullPaintInvalidation();
+ }
+}
+
+PopupMenu* MenuListSelectType::PopupForTesting() const {
+ return popup_.Get();
+}
+
+void MenuListSelectType::DidSelectOption(
+ HTMLOptionElement* element,
+ HTMLSelectElement::SelectOptionFlags flags,
+ bool should_update_popup) {
+ // Need to update last_on_change_option_ before UpdateFromElement().
+ const bool should_dispatch_events =
+ (flags & HTMLSelectElement::kDispatchInputAndChangeEventFlag) &&
+ select_->last_on_change_option_ != element;
+ select_->last_on_change_option_ = element;
+
+ UpdateTextStyleAndContent();
+ // PopupMenu::UpdateFromElement() posts an O(N) task.
+ if (PopupIsVisible() && should_update_popup)
+ popup_->UpdateFromElement(PopupMenu::kBySelectionChange);
+
+ SelectType::DidSelectOption(element, flags, should_update_popup);
+
+ if (should_dispatch_events) {
+ select_->DispatchInputEvent();
+ select_->DispatchChangeEvent();
+ }
+ if (select_->GetLayoutObject()) {
+ // Need to check will_be_destroyed_ because event handlers might
+ // disassociate |this| and select_.
+ if (!will_be_destroyed_) {
+ // DidUpdateActiveOption() is O(N) because of HTMLOptionElement::index().
+ DidUpdateActiveOption(element);
+ }
+ }
+}
+
+void MenuListSelectType::DispatchEventsIfSelectedOptionChanged() {
+ HTMLOptionElement* selected_option = select_->SelectedOption();
+ if (select_->last_on_change_option_.Get() != selected_option) {
+ select_->last_on_change_option_ = selected_option;
+ select_->DispatchInputEvent();
+ select_->DispatchChangeEvent();
+ }
+}
+
+void MenuListSelectType::DidBlur() {
+ // We only need to fire change events here for menu lists, because we fire
+ // change events for list boxes whenever the selection change is actually
+ // made. This matches other browsers' behavior.
+ DispatchEventsIfSelectedOptionChanged();
+ if (PopupIsVisible())
+ HidePopup();
+}
+
+void MenuListSelectType::DidSetSuggestedOption(HTMLOptionElement*) {
+ UpdateTextStyleAndContent();
+ if (PopupIsVisible())
+ popup_->UpdateFromElement(PopupMenu::kBySelectionChange);
+}
+
+void MenuListSelectType::SaveLastSelection() {
+ select_->last_on_change_option_ = select_->SelectedOption();
+}
+
+void MenuListSelectType::DidDetachLayoutTree() {
+ if (popup_)
+ popup_->DisconnectClient();
+ SetPopupIsVisible(false);
+ popup_ = nullptr;
+ UnobserveTreeMutation();
+}
+
+void MenuListSelectType::DidRecalcStyle(const StyleRecalcChange change) {
+ if (change.ReattachLayoutTree())
+ return;
+ UpdateTextStyle();
+ if (PopupIsVisible())
+ popup_->UpdateFromElement(PopupMenu::kByStyleChange);
+}
+
+String MenuListSelectType::UpdateTextStyleInternal() {
+ HTMLOptionElement* option = OptionToBeShown();
+ String text = g_empty_string;
+ const ComputedStyle* option_style = nullptr;
+
+ if (select_->IsMultiple()) {
+ unsigned selected_count = 0;
+ HTMLOptionElement* selected_option_element = nullptr;
+ for (auto* const option : select_->GetOptionList()) {
+ if (option->Selected()) {
+ if (++selected_count == 1)
+ selected_option_element = option;
+ }
+ }
+
+ if (selected_count == 1) {
+ text = selected_option_element->TextIndentedToRespectGroupLabel();
+ option_style = selected_option_element->GetComputedStyle();
+ } else {
+ Locale& locale = select_->GetLocale();
+ String localized_number_string =
+ locale.ConvertToLocalizedNumber(String::Number(selected_count));
+ text = locale.QueryString(IDS_FORM_SELECT_MENU_LIST_TEXT,
+ localized_number_string);
+ DCHECK(!option_style);
+ }
+ } else {
+ if (option) {
+ text = option->TextIndentedToRespectGroupLabel();
+ option_style = option->GetComputedStyle();
+ }
+ }
+ option_style_ = option_style;
+
+ auto& inner_element = select_->InnerElement();
+ const ComputedStyle* inner_style = inner_element.GetComputedStyle();
+ if (inner_style && option_style &&
+ ((option_style->Direction() != inner_style->Direction() ||
+ option_style->GetUnicodeBidi() != inner_style->GetUnicodeBidi()))) {
+ scoped_refptr<ComputedStyle> cloned_style =
+ ComputedStyle::Clone(*inner_style);
+ cloned_style->SetDirection(option_style->Direction());
+ cloned_style->SetUnicodeBidi(option_style->GetUnicodeBidi());
+ if (auto* inner_layout = inner_element.GetLayoutObject()) {
+ inner_layout->SetModifiedStyleOutsideStyleRecalc(
+ std::move(cloned_style), LayoutObject::ApplyStyleChanges::kYes);
+ } else {
+ inner_element.SetComputedStyle(std::move(cloned_style));
+ }
+ }
+ if (select_->GetLayoutObject())
+ DidUpdateActiveOption(option);
+
+ return text.StripWhiteSpace();
+}
+
+void MenuListSelectType::UpdateTextStyleAndContent() {
+ select_->InnerElement().firstChild()->setNodeValue(UpdateTextStyleInternal());
+ if (auto* box = select_->GetLayoutBox()) {
+ if (auto* cache = select_->GetDocument().ExistingAXObjectCache())
+ cache->TextChanged(box);
+ }
+}
+
+void MenuListSelectType::DidUpdateActiveOption(HTMLOptionElement* option) {
+ Document& document = select_->GetDocument();
+ if (!document.ExistingAXObjectCache())
+ return;
+
+ int option_index = option ? option->index() : -1;
+ if (ax_menulist_last_active_index_ == option_index)
+ return;
+ ax_menulist_last_active_index_ = option_index;
+
+ // We skip sending accessiblity notifications for the very first option,
+ // otherwise we get extra focus and select events that are undesired.
+ if (!has_updated_menulist_active_option_) {
+ has_updated_menulist_active_option_ = true;
+ return;
+ }
+
+ document.ExistingAXObjectCache()->HandleUpdateActiveMenuOption(
+ select_->GetLayoutObject(), option_index);
+}
+
+HTMLOptionElement* MenuListSelectType::OptionToBeShown() const {
+ if (auto* option =
+ select_->OptionAtListIndex(select_->index_to_select_on_cancel_))
+ return option;
+ if (select_->suggested_option_)
+ return select_->suggested_option_;
+ // TODO(tkent): We should not call OptionToBeShown() in IsMultiple() case.
+ if (select_->IsMultiple())
+ return select_->SelectedOption();
+ DCHECK_EQ(select_->SelectedOption(), select_->last_on_change_option_);
+ return select_->last_on_change_option_;
+}
+
+void MenuListSelectType::MaximumOptionWidthMightBeChanged() const {
+ if (LayoutObject* layout_object = select_->GetLayoutObject()) {
+ layout_object->SetNeedsLayoutAndIntrinsicWidthsRecalc(
+ layout_invalidation_reason::kMenuOptionsChanged);
+ }
+}
+
+// PopupUpdater notifies updates of the specified SELECT element subtree to
+// a PopupMenu object.
+class PopupUpdater : public MutationObserver::Delegate {
+ public:
+ explicit PopupUpdater(MenuListSelectType& select_type,
+ HTMLSelectElement& select)
+ : select_type_(select_type),
+ select_(select),
+ observer_(MutationObserver::Create(this)) {
+ MutationObserverInit* init = MutationObserverInit::Create();
+ init->setAttributeOldValue(true);
+ init->setAttributes(true);
+ // Observe only attributes which affect popup content.
+ init->setAttributeFilter({"disabled", "label", "selected", "value"});
+ init->setCharacterData(true);
+ init->setCharacterDataOldValue(true);
+ init->setChildList(true);
+ init->setSubtree(true);
+ observer_->observe(select_, init, ASSERT_NO_EXCEPTION);
+ }
+
+ ExecutionContext* GetExecutionContext() const override {
+ return select_->GetExecutionContext();
+ }
+
+ void Deliver(const MutationRecordVector& records,
+ MutationObserver&) override {
+ // We disconnect the MutationObserver when a popup is closed. However
+ // MutationObserver can call back after disconnection.
+ if (!select_type_->PopupIsVisible())
+ return;
+ for (const auto& record : records) {
+ if (record->type() == "attributes") {
+ const auto& element = *To<Element>(record->target());
+ if (record->oldValue() == element.getAttribute(record->attributeName()))
+ continue;
+ } else if (record->type() == "characterData") {
+ if (record->oldValue() == record->target()->nodeValue())
+ continue;
+ }
+ select_type_->DidMutateSubtree();
+ return;
+ }
+ }
+
+ void Dispose() { observer_->disconnect(); }
+
+ void Trace(Visitor* visitor) override {
+ visitor->Trace(select_type_);
+ visitor->Trace(select_);
+ visitor->Trace(observer_);
+ MutationObserver::Delegate::Trace(visitor);
+ }
+
+ private:
+ Member<MenuListSelectType> select_type_;
+ Member<HTMLSelectElement> select_;
+ Member<MutationObserver> observer_;
+};
+
+void MenuListSelectType::ObserveTreeMutation() {
+ DCHECK(!popup_updater_);
+ popup_updater_ = MakeGarbageCollected<PopupUpdater>(*this, *select_);
+}
+
+void MenuListSelectType::UnobserveTreeMutation() {
+ if (!popup_updater_)
+ return;
+ popup_updater_->Dispose();
+ popup_updater_ = nullptr;
+}
+
+void MenuListSelectType::DidMutateSubtree() {
+ DCHECK(PopupIsVisible());
+ DCHECK(popup_);
+ popup_->UpdateFromElement(PopupMenu::kByDOMChange);
+}
+
+// ============================================================================
+
+class ListBoxSelectType final : public SelectType {
+ public:
+ explicit ListBoxSelectType(HTMLSelectElement& select) : SelectType(select) {}
+ bool DefaultEventHandler(const Event& event) override;
+ void DidBlur() override;
+ void DidSetSuggestedOption(HTMLOptionElement* option) override;
+ void SaveLastSelection() override;
+ void SelectAll() override;
+ void SaveListboxActiveSelection() override;
+ void HandleMouseRelease() override;
+ void ListBoxOnChange() override;
+ void ClearLastOnChangeSelection() override;
+
+ private:
+ HTMLOptionElement* NextSelectableOptionPageAway(HTMLOptionElement*,
+ SkipDirection) const;
+ // Update :-internal-multi-select-focus state of selected OPTIONs.
+ void UpdateMultiSelectFocus();
+ void ToggleSelection(HTMLOptionElement& option);
+ enum class SelectionMode {
+ kDeselectOthers,
+ kRange,
+ kNotChangeOthers,
+ };
+ void UpdateSelectedState(HTMLOptionElement* clicked_option,
+ SelectionMode mode);
+ void UpdateListBoxSelection(bool deselect_other_options, bool scroll = true);
+
+ Vector<bool> cached_state_for_active_selection_;
+ Vector<bool> last_on_change_selection_;
+ bool is_in_non_contiguous_selection_ = false;
+ bool active_selection_state_ = false;
+};
+
+bool ListBoxSelectType::DefaultEventHandler(const Event& event) {
+ const auto* mouse_event = DynamicTo<MouseEvent>(event);
+ const auto* gesture_event = DynamicTo<GestureEvent>(event);
+ if (event.type() == event_type_names::kGesturetap && gesture_event) {
+ select_->focus();
+ // Calling focus() may cause us to lose our layoutObject or change the
+ // layoutObject type, in which case do not want to handle the event.
+ if (!select_->GetLayoutObject() || will_be_destroyed_)
+ return false;
+
+ // Convert to coords relative to the list box if needed.
+ if (HTMLOptionElement* option = EventTargetOption(*gesture_event)) {
+ if (!select_->IsDisabledFormControl()) {
+ UpdateSelectedState(option, gesture_event->shiftKey()
+ ? SelectionMode::kRange
+ : SelectionMode::kNotChangeOthers);
+ ListBoxOnChange();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ if (event.type() == event_type_names::kMousedown && mouse_event &&
+ mouse_event->button() ==
+ static_cast<int16_t>(WebPointerProperties::Button::kLeft)) {
+ select_->focus();
+ // Calling focus() may cause us to lose our layoutObject, in which case
+ // do not want to handle the event.
+ if (!select_->GetLayoutObject() || will_be_destroyed_ ||
+ select_->IsDisabledFormControl())
+ return false;
+
+ // Convert to coords relative to the list box if needed.
+ if (HTMLOptionElement* option = EventTargetOption(*mouse_event)) {
+ if (!option->IsDisabledFormControl()) {
+#if defined(OS_MACOSX)
+ const bool meta_or_ctrl = mouse_event->metaKey();
+#else
+ const bool meta_or_ctrl = mouse_event->ctrlKey();
+#endif
+ UpdateSelectedState(option, mouse_event->shiftKey()
+ ? SelectionMode::kRange
+ : meta_or_ctrl
+ ? SelectionMode::kNotChangeOthers
+ : SelectionMode::kDeselectOthers);
+ }
+ if (LocalFrame* frame = select_->GetDocument().GetFrame())
+ frame->GetEventHandler().SetMouseDownMayStartAutoscroll();
+
+ return true;
+ }
+ return false;
+ }
+
+ if (event.type() == event_type_names::kMousemove && mouse_event) {
+ if (mouse_event->button() !=
+ static_cast<int16_t>(WebPointerProperties::Button::kLeft) ||
+ !mouse_event->ButtonDown())
+ return false;
+
+ if (auto* layout_object = select_->GetLayoutObject()) {
+ layout_object->GetFrameView()->UpdateAllLifecyclePhasesExceptPaint(
+ DocumentUpdateReason::kScroll);
+
+ if (Page* page = select_->GetDocument().GetPage()) {
+ page->GetAutoscrollController().StartAutoscrollForSelection(
+ layout_object);
+ }
+ }
+ // Mousedown didn't happen in this element.
+ if (last_on_change_selection_.IsEmpty())
+ return false;
+
+ if (HTMLOptionElement* option = EventTargetOption(*mouse_event)) {
+ if (!select_->IsDisabledFormControl()) {
+ if (select_->is_multiple_) {
+ // Only extend selection if there is something selected.
+ if (!select_->active_selection_anchor_)
+ return false;
+
+ select_->SetActiveSelectionEnd(option);
+ UpdateListBoxSelection(false);
+ } else {
+ select_->SetActiveSelectionAnchor(option);
+ select_->SetActiveSelectionEnd(option);
+ UpdateListBoxSelection(true);
+ }
+ }
+ }
+ return false;
+ }
+
+ if (event.type() == event_type_names::kMouseup && mouse_event &&
+ mouse_event->button() ==
+ static_cast<int16_t>(WebPointerProperties::Button::kLeft) &&
+ select_->GetLayoutObject()) {
+ auto* page = select_->GetDocument().GetPage();
+ if (page && page->GetAutoscrollController().AutoscrollInProgressFor(
+ select_->GetLayoutBox()))
+ page->GetAutoscrollController().StopAutoscroll();
+ else
+ HandleMouseRelease();
+ return false;
+ }
+
+ if (event.type() == event_type_names::kKeydown) {
+ const auto* keyboard_event = DynamicTo<KeyboardEvent>(event);
+ if (!keyboard_event)
+ return false;
+ const String& key = keyboard_event->key();
+
+ bool handled = false;
+ HTMLOptionElement* end_option = nullptr;
+ if (!select_->active_selection_end_) {
+ // Initialize the end index
+ if (key == "ArrowDown" || key == "PageDown") {
+ HTMLOptionElement* start_option = select_->LastSelectedOption();
+ handled = true;
+ if (key == "ArrowDown") {
+ end_option = NextSelectableOption(start_option);
+ } else {
+ end_option =
+ NextSelectableOptionPageAway(start_option, kSkipForwards);
+ }
+ } else if (key == "ArrowUp" || key == "PageUp") {
+ HTMLOptionElement* start_option = select_->SelectedOption();
+ handled = true;
+ if (key == "ArrowUp") {
+ end_option = PreviousSelectableOption(start_option);
+ } else {
+ end_option =
+ NextSelectableOptionPageAway(start_option, kSkipBackwards);
+ }
+ }
+ } else {
+ // Set the end index based on the current end index.
+ if (key == "ArrowDown") {
+ end_option = NextSelectableOption(select_->active_selection_end_.Get());
+ handled = true;
+ } else if (key == "ArrowUp") {
+ end_option =
+ PreviousSelectableOption(select_->active_selection_end_.Get());
+ handled = true;
+ } else if (key == "PageDown") {
+ end_option = NextSelectableOptionPageAway(
+ select_->active_selection_end_.Get(), kSkipForwards);
+ handled = true;
+ } else if (key == "PageUp") {
+ end_option = NextSelectableOptionPageAway(
+ select_->active_selection_end_.Get(), kSkipBackwards);
+ handled = true;
+ }
+ }
+ if (key == "Home") {
+ end_option = FirstSelectableOption();
+ handled = true;
+ } else if (key == "End") {
+ end_option = LastSelectableOption();
+ handled = true;
+ }
+
+ if (IsSpatialNavigationEnabled(select_->GetDocument().GetFrame())) {
+ // Check if the selection moves to the boundary.
+ if (key == "ArrowLeft" || key == "ArrowRight" ||
+ ((key == "ArrowDown" || key == "ArrowUp") &&
+ end_option == select_->active_selection_end_))
+ return false;
+ }
+
+ bool is_control_key = false;
+#if defined(OS_MACOSX)
+ is_control_key = keyboard_event->metaKey();
+#else
+ is_control_key = keyboard_event->ctrlKey();
+#endif
+
+ if (select_->is_multiple_ && keyboard_event->keyCode() == ' ' &&
+ is_control_key && select_->active_selection_end_) {
+ // Use ctrl+space to toggle selection change.
+ ToggleSelection(*select_->active_selection_end_);
+ return true;
+ }
+
+ if (end_option && handled) {
+ // Save the selection so it can be compared to the new selection
+ // when dispatching change events immediately after making the new
+ // selection.
+ SaveLastSelection();
+
+ select_->SetActiveSelectionEnd(end_option);
+
+ is_in_non_contiguous_selection_ = select_->is_multiple_ && is_control_key;
+ bool select_new_item =
+ !select_->is_multiple_ || keyboard_event->shiftKey() ||
+ (!IsSpatialNavigationEnabled(select_->GetDocument().GetFrame()) &&
+ !is_in_non_contiguous_selection_);
+ if (select_new_item)
+ active_selection_state_ = true;
+ // If the anchor is uninitialized, or if we're going to deselect all
+ // other options, then set the anchor index equal to the end index.
+ bool deselect_others = !select_->is_multiple_ ||
+ (!keyboard_event->shiftKey() && select_new_item);
+ if (!select_->active_selection_anchor_ || deselect_others) {
+ if (deselect_others)
+ select_->DeselectItemsWithoutValidation();
+ select_->SetActiveSelectionAnchor(select_->active_selection_end_.Get());
+ }
+
+ select_->ScrollToOption(end_option);
+ if (select_new_item || is_in_non_contiguous_selection_) {
+ if (select_new_item) {
+ UpdateListBoxSelection(deselect_others);
+ ListBoxOnChange();
+ }
+ UpdateMultiSelectFocus();
+ } else {
+ select_->ScrollToSelection();
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ if (event.type() == event_type_names::kKeypress) {
+ auto* keyboard_event = DynamicTo<KeyboardEvent>(event);
+ if (!keyboard_event)
+ return false;
+ int key_code = keyboard_event->keyCode();
+
+ if (key_code == '\r') {
+ if (HTMLFormElement* form = select_->Form())
+ form->SubmitImplicitly(event, false);
+ return true;
+ } else if (select_->is_multiple_ && key_code == ' ' &&
+ (IsSpatialNavigationEnabled(select_->GetDocument().GetFrame()) ||
+ is_in_non_contiguous_selection_)) {
+ HTMLOptionElement* option = select_->active_selection_end_;
+ // If there's no active selection,
+ // act as if "ArrowDown" had been pressed.
+ if (!option)
+ option = NextSelectableOption(select_->LastSelectedOption());
+ if (option) {
+ // Use space to toggle selection change.
+ ToggleSelection(*option);
+ return true;
+ }
+ }
+ return false;
+ }
+ return false;
+}
+
+void ListBoxSelectType::DidBlur() {
+ ClearLastOnChangeSelection();
+}
+
+void ListBoxSelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
+ if (select_->GetLayoutObject())
+ select_->ScrollToOption(option);
+}
+
+void ListBoxSelectType::SaveLastSelection() {
+ last_on_change_selection_.clear();
+ for (auto& element : select_->GetListItems()) {
+ auto* option_element = DynamicTo<HTMLOptionElement>(element.Get());
+ last_on_change_selection_.push_back(option_element &&
+ option_element->Selected());
+ }
+}
+
+void ListBoxSelectType::UpdateMultiSelectFocus() {
+ if (!select_->is_multiple_)
+ return;
+
+ for (auto* const option : select_->GetOptionList()) {
+ if (option->IsDisabledFormControl() || !option->GetLayoutObject())
+ continue;
+ bool is_focused = (option == select_->active_selection_end_) &&
+ is_in_non_contiguous_selection_;
+ option->SetMultiSelectFocusedState(is_focused);
+ }
+ select_->ScrollToSelection();
+}
+
+void ListBoxSelectType::SelectAll() {
+ if (!select_->GetLayoutObject() || !select_->is_multiple_)
+ return;
+
+ // Save the selection so it can be compared to the new selectAll selection
+ // when dispatching change events.
+ SaveLastSelection();
+
+ active_selection_state_ = true;
+ select_->SetActiveSelectionAnchor(NextSelectableOption(nullptr));
+ select_->SetActiveSelectionEnd(PreviousSelectableOption(nullptr));
+
+ UpdateListBoxSelection(false, false);
+ ListBoxOnChange();
+ select_->SetNeedsValidityCheck();
+}
+
+// Returns the index of the next valid item one page away from |start_option|
+// in direction |direction|.
+HTMLOptionElement* ListBoxSelectType::NextSelectableOptionPageAway(
+ HTMLOptionElement* start_option,
+ SkipDirection direction) const {
+ const auto& items = select_->GetListItems();
+ // -1 so we still show context.
+ int page_size = select_->ListBoxSize() - 1;
+
+ // One page away, but not outside valid bounds.
+ // If there is a valid option item one page away, the index is chosen.
+ // If there is no exact one page away valid option, returns start_index or
+ // the most far index.
+ int start_index = start_option ? start_option->ListIndex() : -1;
+ int edge_index = (direction == kSkipForwards) ? 0 : (items.size() - 1);
+ int skip_amount =
+ page_size +
+ ((direction == kSkipForwards) ? start_index : (edge_index - start_index));
+ return NextValidOption(edge_index, direction, skip_amount);
+}
+
+void ListBoxSelectType::ToggleSelection(HTMLOptionElement& option) {
+ active_selection_state_ = !active_selection_state_;
+ UpdateSelectedState(&option, SelectionMode::kNotChangeOthers);
+ ListBoxOnChange();
+}
+
+void ListBoxSelectType::UpdateSelectedState(HTMLOptionElement* clicked_option,
+ SelectionMode mode) {
+ DCHECK(clicked_option);
+ // Save the selection so it can be compared to the new selection when
+ // dispatching change events during mouseup, or after autoscroll finishes.
+ SaveLastSelection();
+
+ active_selection_state_ = true;
+
+ if (!select_->is_multiple_)
+ mode = SelectionMode::kDeselectOthers;
+
+ // Keep track of whether an active selection (like during drag selection),
+ // should select or deselect.
+ if (clicked_option->Selected() && mode == SelectionMode::kNotChangeOthers) {
+ active_selection_state_ = false;
+ clicked_option->SetSelectedState(false);
+ clicked_option->SetDirty(true);
+ }
+
+ // If we're not in any special multiple selection mode, then deselect all
+ // other items, excluding the clicked OPTION. If no option was clicked, then
+ // this will deselect all items in the list.
+ if (mode == SelectionMode::kDeselectOthers)
+ select_->DeselectItemsWithoutValidation(clicked_option);
+
+ // If the anchor hasn't been set, and we're doing kDeselectOthers or kRange,
+ // then initialize the anchor to the first selected OPTION.
+ if (!select_->active_selection_anchor_ &&
+ mode != SelectionMode::kNotChangeOthers)
+ select_->SetActiveSelectionAnchor(select_->SelectedOption());
+
+ // Set the selection state of the clicked OPTION.
+ if (!clicked_option->IsDisabledFormControl()) {
+ clicked_option->SetSelectedState(true);
+ clicked_option->SetDirty(true);
+ }
+
+ // If there was no selectedIndex() for the previous initialization, or if
+ // we're doing kDeselectOthers, or kNotChangeOthers (using cmd or ctrl),
+ // then initialize the anchor OPTION to the clicked OPTION.
+ if (!select_->active_selection_anchor_ || mode != SelectionMode::kRange)
+ select_->SetActiveSelectionAnchor(clicked_option);
+
+ select_->SetActiveSelectionEnd(clicked_option);
+ UpdateListBoxSelection(mode != SelectionMode::kNotChangeOthers);
+}
+
+void ListBoxSelectType::UpdateListBoxSelection(bool deselect_other_options,
+ bool scroll) {
+ DCHECK(select_->GetLayoutObject());
+ HTMLOptionElement* const anchor_option = select_->active_selection_anchor_;
+ HTMLOptionElement* const end_option = select_->active_selection_end_;
+ const int anchor_index = anchor_option ? anchor_option->index() : -1;
+ const int end_index = end_option ? end_option->index() : -1;
+ const int start = std::min(anchor_index, end_index);
+ const int end = std::max(anchor_index, end_index);
+
+ int i = 0;
+ for (auto* const option : select_->GetOptionList()) {
+ if (option->IsDisabledFormControl() || !option->GetLayoutObject()) {
+ ++i;
+ continue;
+ }
+ if (i >= start && i <= end) {
+ option->SetSelectedState(active_selection_state_);
+ option->SetDirty(true);
+ } else if (deselect_other_options ||
+ i >= static_cast<int>(
+ cached_state_for_active_selection_.size())) {
+ option->SetSelectedState(false);
+ option->SetDirty(true);
+ } else {
+ option->SetSelectedState(cached_state_for_active_selection_[i]);
+ }
+ ++i;
+ }
+
+ UpdateMultiSelectFocus();
+ select_->SetNeedsValidityCheck();
+ if (scroll)
+ select_->ScrollToSelection();
+ select_->NotifyFormStateChanged();
+}
+
+void ListBoxSelectType::SaveListboxActiveSelection() {
+ // Cache the selection state so we can restore the old selection as the new
+ // selection pivots around this anchor index.
+ // Example:
+ // 1. Press the mouse button on the second OPTION
+ // active_selection_anchor_ points the second OPTION.
+ // 2. Drag the mouse pointer onto the fifth OPTION
+ // active_selection_end_ points the fifth OPTION, OPTIONs at 1-4 indices
+ // are selected.
+ // 3. Drag the mouse pointer onto the fourth OPTION
+ // active_selection_end_ points the fourth OPTION, OPTIONs at 1-3 indices
+ // are selected.
+ // UpdateListBoxSelection needs to clear selection of the fifth OPTION.
+ cached_state_for_active_selection_.resize(0);
+ for (auto* const option : select_->GetOptionList()) {
+ cached_state_for_active_selection_.push_back(option->Selected());
+ }
+}
+
+void ListBoxSelectType::HandleMouseRelease() {
+ // We didn't start this click/drag on any options.
+ if (last_on_change_selection_.IsEmpty())
+ return;
+ ListBoxOnChange();
+}
+
+void ListBoxSelectType::ListBoxOnChange() {
+ const auto& items = select_->GetListItems();
+
+ // If the cached selection list is empty, or the size has changed, then fire
+ // 'change' event, and return early.
+ // FIXME: Why? This looks unreasonable.
+ if (last_on_change_selection_.IsEmpty() ||
+ last_on_change_selection_.size() != items.size()) {
+ select_->DispatchChangeEvent();
+ return;
+ }
+
+ // Update last_on_change_selection_ and fire a 'change' event.
+ bool fire_on_change = false;
+ for (unsigned i = 0; i < items.size(); ++i) {
+ HTMLElement* element = items[i];
+ auto* option_element = DynamicTo<HTMLOptionElement>(element);
+ bool selected = option_element && option_element->Selected();
+ if (selected != last_on_change_selection_[i])
+ fire_on_change = true;
+ last_on_change_selection_[i] = selected;
+ }
+
+ if (fire_on_change) {
+ select_->DispatchInputEvent();
+ select_->DispatchChangeEvent();
+ }
+}
+
+void ListBoxSelectType::ClearLastOnChangeSelection() {
+ last_on_change_selection_.clear();
+}
+
+// ============================================================================
+
+SelectType::SelectType(HTMLSelectElement& select) : select_(select) {}
+
+SelectType* SelectType::Create(HTMLSelectElement& select) {
+ if (select.UsesMenuList())
+ return MakeGarbageCollected<MenuListSelectType>(select);
+ else
+ return MakeGarbageCollected<ListBoxSelectType>(select);
+}
+
+void SelectType::WillBeDestroyed() {
+ will_be_destroyed_ = true;
+}
+
+void SelectType::Trace(Visitor* visitor) {
+ visitor->Trace(select_);
+}
+
+void SelectType::DidSelectOption(HTMLOptionElement*,
+ HTMLSelectElement::SelectOptionFlags,
+ bool) {
+ select_->ScrollToSelection();
+ select_->SetNeedsValidityCheck();
+}
+
+void SelectType::DidDetachLayoutTree() {}
+
+void SelectType::DidRecalcStyle(const StyleRecalcChange) {}
+
+void SelectType::UpdateTextStyle() {}
+
+void SelectType::UpdateTextStyleAndContent() {}
+
+HTMLOptionElement* SelectType::OptionToBeShown() const {
+ NOTREACHED();
+ return nullptr;
+}
+
+const ComputedStyle* SelectType::OptionStyle() const {
+ NOTREACHED();
+ return nullptr;
+}
+
+void SelectType::MaximumOptionWidthMightBeChanged() const {}
+
+void SelectType::SelectAll() {
+ NOTREACHED();
+}
+
+void SelectType::SaveListboxActiveSelection() {}
+
+void SelectType::HandleMouseRelease() {}
+
+void SelectType::ListBoxOnChange() {}
+
+void SelectType::ClearLastOnChangeSelection() {}
+
+void SelectType::ShowPopup() {
+ NOTREACHED();
+}
+
+void SelectType::HidePopup() {
+ NOTREACHED();
+}
+
+void SelectType::PopupDidHide() {
+ NOTREACHED();
+}
+
+bool SelectType::PopupIsVisible() const {
+ return false;
+}
+
+PopupMenu* SelectType::PopupForTesting() const {
+ NOTREACHED();
+ return nullptr;
+}
+
+// Returns the 1st valid OPTION |skip| items from |list_index| in direction
+// |direction| if there is one.
+// Otherwise, it returns the valid OPTION closest to that boundary which is past
+// |list_index| if there is one.
+// Otherwise, it returns nullptr.
+// Valid means that it is enabled and visible.
+HTMLOptionElement* SelectType::NextValidOption(int list_index,
+ SkipDirection direction,
+ int skip) const {
+ DCHECK(direction == kSkipBackwards || direction == kSkipForwards);
+ const auto& list_items = select_->GetListItems();
+ HTMLOptionElement* last_good_option = nullptr;
+ int size = list_items.size();
+ for (list_index += direction; list_index >= 0 && list_index < size;
+ list_index += direction) {
+ --skip;
+ HTMLElement* element = list_items[list_index];
+ auto* option_element = DynamicTo<HTMLOptionElement>(element);
+ if (!option_element)
+ continue;
+ if (option_element->IsDisplayNone())
+ continue;
+ if (element->IsDisabledFormControl())
+ continue;
+ if (!select_->UsesMenuList() && !element->GetLayoutObject())
+ continue;
+ last_good_option = option_element;
+ if (skip <= 0)
+ break;
+ }
+ return last_good_option;
+}
+
+HTMLOptionElement* SelectType::NextSelectableOption(
+ HTMLOptionElement* start_option) const {
+ return NextValidOption(start_option ? start_option->ListIndex() : -1,
+ kSkipForwards, 1);
+}
+
+HTMLOptionElement* SelectType::PreviousSelectableOption(
+ HTMLOptionElement* start_option) const {
+ return NextValidOption(
+ start_option ? start_option->ListIndex() : select_->GetListItems().size(),
+ kSkipBackwards, 1);
+}
+
+HTMLOptionElement* SelectType::FirstSelectableOption() const {
+ return NextValidOption(-1, kSkipForwards, 1);
+}
+
+HTMLOptionElement* SelectType::LastSelectableOption() const {
+ return NextValidOption(select_->GetListItems().size(), kSkipBackwards, 1);
+}
+
+} // namespace blink