blob: 3a02df56a8e7c092372a108481f1bc55cccad8f3 [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/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/focus_params.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/dom/text.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/menu_list_inner_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/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/scroll/scroll_alignment.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) const 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 CreateShadowSubtree(ShadowRoot& root) override;
Element& InnerElement() const override;
void ShowPopup() override;
void HidePopup() override;
void PopupDidHide() override;
bool PopupIsVisible() const override;
PopupMenu* PopupForTesting() const override;
AXObject* PopupRootAXObject() 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) const {
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::kMouse,
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")) ||
((key == "ArrowDown" || key == "ArrowUp") && event.altKey()) ||
((!event.altKey() && !event.ctrlKey() && key == "F4")));
}
bool MenuListSelectType::ShouldOpenPopupForKeyPressEvent(
const KeyboardEvent& event) {
LayoutTheme& layout_theme = LayoutTheme::GetTheme();
int key_code = event.keyCode();
return ((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::CreateShadowSubtree(ShadowRoot& root) {
Document& doc = select_->GetDocument();
Element* inner_element = MakeGarbageCollected<MenuListInnerElement>(doc);
inner_element->setAttribute(html_names::kAriaHiddenAttr, "true");
// Make sure InnerElement() always has a Text node.
inner_element->appendChild(Text::Create(doc, g_empty_string));
root.insertBefore(inner_element, root.firstChild());
}
Element& MenuListSelectType::InnerElement() const {
auto* inner_element =
DynamicTo<Element>(select_->UserAgentShadowRoot()->firstChild());
DCHECK(inner_element);
return *inner_element;
}
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();
}
AXObject* MenuListSelectType::PopupRootAXObject() const {
return popup_ ? popup_->PopupRootAXObject() : nullptr;
}
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);
select_->SetNeedsValidityCheck();
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_to_be_shown = 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_to_be_shown) {
text = option_to_be_shown->TextIndentedToRespectGroupLabel();
option_style = option_to_be_shown->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_to_be_shown);
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) const 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) {}
void Trace(Visitor* visitor) const override;
bool DefaultEventHandler(const Event& event) override;
void DidSelectOption(HTMLOptionElement* element,
HTMLSelectElement::SelectOptionFlags flags,
bool should_update_popup) override;
void OptionRemoved(HTMLOptionElement& option) override;
void DidBlur() override;
void DidSetSuggestedOption(HTMLOptionElement* option) override;
void SaveLastSelection() override;
HTMLOptionElement* SpatialNavigationFocusedOption() override;
HTMLOptionElement* ActiveSelectionEnd() const override;
void ScrollToSelection() override;
void ScrollToOption(HTMLOptionElement* option) 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);
void SetActiveSelectionAnchor(HTMLOptionElement*);
void SetActiveSelectionEnd(HTMLOptionElement*);
void ScrollToOptionTask();
Vector<bool> cached_state_for_active_selection_;
Vector<bool> last_on_change_selection_;
Member<HTMLOptionElement> option_to_scroll_to_;
Member<HTMLOptionElement> active_selection_anchor_;
Member<HTMLOptionElement> active_selection_end_;
bool is_in_non_contiguous_selection_ = false;
bool active_selection_state_ = false;
};
void ListBoxSelectType::Trace(Visitor* visitor) const {
visitor->Trace(option_to_scroll_to_);
visitor->Trace(active_selection_anchor_);
visitor->Trace(active_selection_end_);
SelectType::Trace(visitor);
}
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_MAC)
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(
select_->GetLayoutObject());
}
}
// 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 (!active_selection_anchor_)
return false;
SetActiveSelectionEnd(option);
UpdateListBoxSelection(false);
} else {
SetActiveSelectionAnchor(option);
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 (!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(active_selection_end_);
handled = true;
} else if (key == "ArrowUp") {
end_option = PreviousSelectableOption(active_selection_end_);
handled = true;
} else if (key == "PageDown") {
end_option =
NextSelectableOptionPageAway(active_selection_end_, kSkipForwards);
handled = true;
} else if (key == "PageUp") {
end_option =
NextSelectableOptionPageAway(active_selection_end_, 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 == active_selection_end_))
return false;
}
bool is_control_key = false;
#if defined(OS_MAC)
is_control_key = keyboard_event->metaKey();
#else
is_control_key = keyboard_event->ctrlKey();
#endif
if (select_->is_multiple_ && keyboard_event->keyCode() == ' ' &&
is_control_key && active_selection_end_) {
// Use ctrl+space to toggle selection change.
ToggleSelection(*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();
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 (!active_selection_anchor_ || deselect_others) {
if (deselect_others)
select_->DeselectItemsWithoutValidation();
SetActiveSelectionAnchor(active_selection_end_.Get());
}
ScrollToOption(end_option);
if (select_new_item || is_in_non_contiguous_selection_) {
if (select_new_item) {
UpdateListBoxSelection(deselect_others);
ListBoxOnChange();
}
UpdateMultiSelectFocus();
} else {
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 = 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::DidSelectOption(
HTMLOptionElement* element,
HTMLSelectElement::SelectOptionFlags flags,
bool should_update_popup) {
// We should update active selection after finishing OPTION state change
// because SetActiveSelectionAnchor() stores OPTION's selection state.
if (element) {
const bool is_single = !select_->IsMultiple();
const bool deselect_other_options =
flags & HTMLSelectElement::kDeselectOtherOptionsFlag;
// SetActiveSelectionAnchor is O(N).
if (!active_selection_anchor_ || is_single || deselect_other_options)
SetActiveSelectionAnchor(element);
if (!active_selection_end_ || is_single || deselect_other_options)
SetActiveSelectionEnd(element);
}
ScrollToSelection();
select_->SetNeedsValidityCheck();
}
void ListBoxSelectType::OptionRemoved(HTMLOptionElement& option) {
if (option_to_scroll_to_ == &option)
option_to_scroll_to_.Clear();
if (active_selection_anchor_ == &option)
active_selection_anchor_.Clear();
if (active_selection_end_ == &option)
active_selection_end_.Clear();
}
void ListBoxSelectType::DidBlur() {
ClearLastOnChangeSelection();
}
void ListBoxSelectType::DidSetSuggestedOption(HTMLOptionElement* option) {
if (select_->GetLayoutObject())
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 == active_selection_end_) && is_in_non_contiguous_selection_;
option->SetMultiSelectFocusedState(is_focused);
}
ScrollToSelection();
}
HTMLOptionElement* ListBoxSelectType::SpatialNavigationFocusedOption() {
if (!IsSpatialNavigationEnabled(select_->GetDocument().GetFrame()))
return nullptr;
if (HTMLOptionElement* option = ActiveSelectionEnd())
return option;
return FirstSelectableOption();
}
void ListBoxSelectType::SetActiveSelectionAnchor(HTMLOptionElement* option) {
active_selection_anchor_ = option;
SaveListboxActiveSelection();
}
void ListBoxSelectType::SetActiveSelectionEnd(HTMLOptionElement* option) {
active_selection_end_ = option;
}
HTMLOptionElement* ListBoxSelectType::ActiveSelectionEnd() const {
if (active_selection_end_)
return active_selection_end_;
return select_->LastSelectedOption();
}
void ListBoxSelectType::ScrollToSelection() {
if (!select_->IsFinishedParsingChildren())
return;
ScrollToOption(ActiveSelectionEnd());
if (AXObjectCache* cache = select_->GetDocument().ExistingAXObjectCache())
cache->ListboxActiveIndexChanged(select_);
}
void ListBoxSelectType::ScrollToOption(HTMLOptionElement* option) {
if (!option)
return;
bool has_pending_task = option_to_scroll_to_;
// We'd like to keep an HTMLOptionElement reference rather than the index of
// the option because the task should work even if unselected option is
// inserted before executing ScrollToOptionTask().
option_to_scroll_to_ = option;
if (!has_pending_task) {
select_->GetDocument()
.GetTaskRunner(TaskType::kUserInteraction)
->PostTask(FROM_HERE, WTF::Bind(&ListBoxSelectType::ScrollToOptionTask,
WrapPersistent(this)));
}
}
void ListBoxSelectType::ScrollToOptionTask() {
HTMLOptionElement* option = option_to_scroll_to_.Release();
if (!option || !select_->isConnected() || will_be_destroyed_)
return;
// OptionRemoved() makes sure option_to_scroll_to_ doesn't have an option
// with another owner.
DCHECK_EQ(option->OwnerSelectElement(), select_);
select_->GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kScroll);
if (!select_->GetLayoutObject())
return;
PhysicalRect bounds = option->BoundingBoxForScrollIntoView();
// The following code will not scroll parent boxes unlike ScrollRectToVisible.
auto* box = select_->GetLayoutBox();
if (!box->IsScrollContainer())
return;
DCHECK(box->Layer());
DCHECK(box->Layer()->GetScrollableArea());
box->Layer()->GetScrollableArea()->ScrollIntoView(
bounds,
ScrollAlignment::CreateScrollIntoViewParams(
ScrollAlignment::ToEdgeIfNeeded(), ScrollAlignment::ToEdgeIfNeeded(),
mojom::blink::ScrollType::kProgrammatic, false,
mojom::blink::ScrollBehavior::kInstant));
}
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;
SetActiveSelectionAnchor(NextSelectableOption(nullptr));
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 (!active_selection_anchor_ && mode != SelectionMode::kNotChangeOthers)
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 (!active_selection_anchor_ || mode != SelectionMode::kRange)
SetActiveSelectionAnchor(clicked_option);
SetActiveSelectionEnd(clicked_option);
UpdateListBoxSelection(mode != SelectionMode::kNotChangeOthers);
}
void ListBoxSelectType::UpdateListBoxSelection(bool deselect_other_options,
bool scroll) {
DCHECK(select_->GetLayoutObject());
HTMLOptionElement* const anchor_option = active_selection_anchor_;
HTMLOptionElement* const end_option = 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)
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) const {
visitor->Trace(select_);
}
void SelectType::OptionRemoved(HTMLOptionElement& option) {}
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 {}
HTMLOptionElement* SelectType::SpatialNavigationFocusedOption() {
return nullptr;
}
HTMLOptionElement* SelectType::ActiveSelectionEnd() const {
NOTREACHED();
return nullptr;
}
void SelectType::ScrollToSelection() {}
void SelectType::ScrollToOption(HTMLOptionElement* option) {}
void SelectType::SelectAll() {
NOTREACHED();
}
void SelectType::SaveListboxActiveSelection() {}
void SelectType::HandleMouseRelease() {}
void SelectType::ListBoxOnChange() {}
void SelectType::ClearLastOnChangeSelection() {}
void SelectType::CreateShadowSubtree(ShadowRoot& root) {}
Element& SelectType::InnerElement() const {
NOTREACHED();
// Returning select_ doesn't make sense, but we need to return an element
// to compile this source. This function must not be called.
return *select_;
}
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;
}
AXObject* SelectType::PopupRootAXObject() 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