blob: eb64c4cdfb76a6e8f3e49fb88afbc9bd402af17a [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/html/forms/slider_thumb_element.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/events/mouse_event.h"
#include "third_party/blink/renderer/core/events/touch_event.h"
#include "third_party/blink/renderer/core/frame/event_handler_registry.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/step_range.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/input/event_handler.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_slider_container.h"
#include "third_party/blink/renderer/core/layout/layout_theme.h"
namespace blink {
using namespace html_names;
inline static bool HasVerticalAppearance(HTMLInputElement* input) {
return input->ComputedStyleRef().Appearance() == kSliderVerticalPart;
}
inline SliderThumbElement::SliderThumbElement(Document& document)
: HTMLDivElement(document), in_drag_mode_(false) {
SetHasCustomStyleCallbacks();
}
SliderThumbElement* SliderThumbElement::Create(Document& document) {
SliderThumbElement* element =
MakeGarbageCollected<SliderThumbElement>(document);
element->setAttribute(kIdAttr, shadow_element_names::SliderThumb());
return element;
}
void SliderThumbElement::SetPositionFromValue() {
// Since the code to calculate position is in the LayoutSliderThumb layout
// path, we don't actually update the value here. Instead, we poke at the
// layoutObject directly to trigger layout.
if (GetLayoutObject()) {
GetLayoutObject()->SetNeedsLayoutAndFullPaintInvalidation(
layout_invalidation_reason::kSliderValueChanged);
}
}
LayoutObject* SliderThumbElement::CreateLayoutObject(
const ComputedStyle& style) {
return LayoutObjectFactory::CreateBlockFlow(*this, style);
}
bool SliderThumbElement::IsDisabledFormControl() const {
return HostInput() && HostInput()->IsDisabledFormControl();
}
bool SliderThumbElement::MatchesReadOnlyPseudoClass() const {
return HostInput() && HostInput()->MatchesReadOnlyPseudoClass();
}
bool SliderThumbElement::MatchesReadWritePseudoClass() const {
return HostInput() && HostInput()->MatchesReadWritePseudoClass();
}
const Node* SliderThumbElement::FocusDelegate() const {
return HostInput();
}
void SliderThumbElement::DragFrom(const LayoutPoint& point) {
StartDragging();
SetPositionFromPoint(point);
}
void SliderThumbElement::SetPositionFromPoint(const LayoutPoint& point) {
HTMLInputElement* input(HostInput());
Element* track_element = input->UserAgentShadowRoot()->getElementById(
shadow_element_names::SliderTrack());
if (!input->GetLayoutObject() || !GetLayoutBox() ||
!track_element->GetLayoutBox())
return;
LayoutPoint offset = LayoutPoint(input->GetLayoutObject()->AbsoluteToLocal(
FloatPoint(point), kUseTransforms));
bool is_vertical = HasVerticalAppearance(input);
bool is_left_to_right_direction =
GetLayoutBox()->Style()->IsLeftToRightDirection();
LayoutUnit track_size;
LayoutUnit position;
LayoutUnit current_position;
// We need to calculate currentPosition from absolute points becaue the
// layoutObject for this node is usually on a layer and layoutBox()->x() and
// y() are unusable.
// FIXME: This should probably respect transforms.
LayoutPoint absolute_thumb_origin =
GetLayoutBox()->AbsoluteBoundingBoxRectIgnoringTransforms().Location();
LayoutPoint absolute_slider_content_origin =
LayoutPoint(input->GetLayoutObject()->LocalToAbsolute());
IntRect track_bounding_box =
track_element->GetLayoutObject()
->AbsoluteBoundingBoxRectIgnoringTransforms();
IntRect input_bounding_box =
input->GetLayoutObject()->AbsoluteBoundingBoxRectIgnoringTransforms();
if (is_vertical) {
track_size = track_element->GetLayoutBox()->ContentHeight() -
GetLayoutBox()->Size().Height();
position = offset.Y() - GetLayoutBox()->Size().Height() / 2 -
track_bounding_box.Y() + input_bounding_box.Y() -
GetLayoutBox()->MarginBottom();
current_position =
absolute_thumb_origin.Y() - absolute_slider_content_origin.Y();
} else {
track_size = track_element->GetLayoutBox()->ContentWidth() -
GetLayoutBox()->Size().Width();
position = offset.X() - GetLayoutBox()->Size().Width() / 2 -
track_bounding_box.X() + input_bounding_box.X();
position -= is_left_to_right_direction ? GetLayoutBox()->MarginLeft()
: GetLayoutBox()->MarginRight();
current_position =
absolute_thumb_origin.X() - absolute_slider_content_origin.X();
}
position = std::min(position, track_size).ClampNegativeToZero();
const Decimal ratio =
Decimal::FromDouble(static_cast<double>(position) / track_size);
const Decimal fraction =
is_vertical || !is_left_to_right_direction ? Decimal(1) - ratio : ratio;
StepRange step_range(input->CreateStepRange(kRejectAny));
Decimal value =
step_range.ClampValue(step_range.ValueFromProportion(fraction));
Decimal closest = input->FindClosestTickMarkValue(value);
if (closest.IsFinite()) {
double closest_fraction =
step_range.ProportionFromValue(closest).ToDouble();
double closest_ratio = is_vertical || !is_left_to_right_direction
? 1.0 - closest_fraction
: closest_fraction;
LayoutUnit closest_position(track_size * closest_ratio);
const LayoutUnit snapping_threshold(5);
if ((closest_position - position).Abs() <= snapping_threshold)
value = closest;
}
String value_string = SerializeForNumberType(value);
if (value_string == input->value())
return;
// FIXME: This is no longer being set from renderer. Consider updating the
// method name.
input->SetValueFromRenderer(value_string);
if (GetLayoutObject()) {
GetLayoutObject()->SetNeedsLayoutAndFullPaintInvalidation(
layout_invalidation_reason::kSliderValueChanged);
}
}
void SliderThumbElement::StartDragging() {
if (LocalFrame* frame = GetDocument().GetFrame()) {
// Note that we get to here only we through mouse event path. The touch
// events are implicitly captured to the starting element and will be
// handled in handleTouchEvent function.
frame->GetEventHandler().SetPointerCapture(PointerEventFactory::kMouseId,
this);
in_drag_mode_ = true;
}
}
void SliderThumbElement::StopDragging() {
if (!in_drag_mode_)
return;
if (LocalFrame* frame = GetDocument().GetFrame()) {
frame->GetEventHandler().ReleasePointerCapture(
PointerEventFactory::kMouseId, this);
}
in_drag_mode_ = false;
if (GetLayoutObject()) {
GetLayoutObject()->SetNeedsLayoutAndFullPaintInvalidation(
layout_invalidation_reason::kSliderValueChanged);
}
if (HostInput())
HostInput()->DispatchFormControlChangeEvent();
}
void SliderThumbElement::DefaultEventHandler(Event& event) {
if (event.IsPointerEvent() &&
event.type() == event_type_names::kLostpointercapture) {
StopDragging();
return;
}
if (!event.IsMouseEvent()) {
HTMLDivElement::DefaultEventHandler(event);
return;
}
// FIXME: Should handle this readonly/disabled check in more general way.
// Missing this kind of check is likely to occur elsewhere if adding it in
// each shadow element.
HTMLInputElement* input = HostInput();
if (!input || input->IsDisabledFormControl()) {
StopDragging();
HTMLDivElement::DefaultEventHandler(event);
return;
}
auto& mouse_event = ToMouseEvent(event);
bool is_left_button = mouse_event.button() ==
static_cast<short>(WebPointerProperties::Button::kLeft);
const AtomicString& event_type = event.type();
// We intentionally do not call event->setDefaultHandled() here because
// MediaControlTimelineElement::defaultEventHandler() wants to handle these
// mouse events.
if (event_type == event_type_names::kMousedown && is_left_button) {
StartDragging();
return;
}
if (event_type == event_type_names::kMouseup && is_left_button) {
StopDragging();
return;
}
if (event_type == event_type_names::kMousemove) {
if (in_drag_mode_)
SetPositionFromPoint(LayoutPoint(mouse_event.AbsoluteLocation()));
return;
}
HTMLDivElement::DefaultEventHandler(event);
}
bool SliderThumbElement::WillRespondToMouseMoveEvents() {
const HTMLInputElement* input = HostInput();
if (input && !input->IsDisabledFormControl() && in_drag_mode_)
return true;
return HTMLDivElement::WillRespondToMouseMoveEvents();
}
bool SliderThumbElement::WillRespondToMouseClickEvents() {
const HTMLInputElement* input = HostInput();
if (input && !input->IsDisabledFormControl())
return true;
return HTMLDivElement::WillRespondToMouseClickEvents();
}
void SliderThumbElement::DetachLayoutTree(const AttachContext& context) {
if (in_drag_mode_) {
if (LocalFrame* frame = GetDocument().GetFrame())
frame->GetEventHandler().SetCapturingMouseEventsElement(nullptr);
}
HTMLDivElement::DetachLayoutTree(context);
}
HTMLInputElement* SliderThumbElement::HostInput() const {
// Only HTMLInputElement creates SliderThumbElement instances as its shadow
// nodes. So, ownerShadowHost() must be an HTMLInputElement.
return ToHTMLInputElement(OwnerShadowHost());
}
static const AtomicString& SliderThumbShadowPartId() {
DEFINE_STATIC_LOCAL(const AtomicString, slider_thumb,
("-webkit-slider-thumb"));
return slider_thumb;
}
static const AtomicString& MediaSliderThumbShadowPartId() {
DEFINE_STATIC_LOCAL(const AtomicString, media_slider_thumb,
("-webkit-media-slider-thumb"));
return media_slider_thumb;
}
const AtomicString& SliderThumbElement::ShadowPseudoId() const {
HTMLInputElement* input = HostInput();
if (!input || !input->GetLayoutObject())
return SliderThumbShadowPartId();
const ComputedStyle& slider_style = input->GetLayoutObject()->StyleRef();
switch (slider_style.Appearance()) {
case kMediaSliderPart:
case kMediaSliderThumbPart:
case kMediaVolumeSliderPart:
case kMediaVolumeSliderThumbPart:
return MediaSliderThumbShadowPartId();
default:
return SliderThumbShadowPartId();
}
}
scoped_refptr<ComputedStyle> SliderThumbElement::CustomStyleForLayoutObject() {
Element* host = OwnerShadowHost();
DCHECK(host);
const ComputedStyle& host_style = host->ComputedStyleRef();
scoped_refptr<ComputedStyle> style = OriginalStyleForLayoutObject();
if (host_style.Appearance() == kSliderVerticalPart)
style->SetAppearance(kSliderThumbVerticalPart);
else if (host_style.Appearance() == kSliderHorizontalPart)
style->SetAppearance(kSliderThumbHorizontalPart);
else if (host_style.Appearance() == kMediaSliderPart)
style->SetAppearance(kMediaSliderThumbPart);
else if (host_style.Appearance() == kMediaVolumeSliderPart)
style->SetAppearance(kMediaVolumeSliderThumbPart);
if (style->HasAppearance())
LayoutTheme::GetTheme().AdjustSliderThumbSize(*style);
return style;
}
// --------------------------------
inline SliderContainerElement::SliderContainerElement(Document& document)
: HTMLDivElement(document),
has_touch_event_handler_(false),
touch_started_(false),
sliding_direction_(kNoMove) {
UpdateTouchEventHandlerRegistry();
SetHasCustomStyleCallbacks();
}
DEFINE_NODE_FACTORY(SliderContainerElement)
HTMLInputElement* SliderContainerElement::HostInput() const {
return ToHTMLInputElement(OwnerShadowHost());
}
LayoutObject* SliderContainerElement::CreateLayoutObject(const ComputedStyle&) {
return new LayoutSliderContainer(this);
}
void SliderContainerElement::DefaultEventHandler(Event& event) {
if (event.IsTouchEvent()) {
HandleTouchEvent(ToTouchEvent(&event));
return;
}
}
void SliderContainerElement::HandleTouchEvent(TouchEvent* event) {
HTMLInputElement* input = HostInput();
if (!input || input->IsDisabledFormControl() || !event)
return;
if (event->type() == event_type_names::kTouchend) {
// TODO: Also do this for touchcancel?
input->DispatchFormControlChangeEvent();
event->SetDefaultHandled();
sliding_direction_ = kNoMove;
touch_started_ = false;
return;
}
// The direction of this series of touch actions has been determined, which is
// perpendicular to the slider, so no need to adjust the value.
if (!CanSlide()) {
return;
}
TouchList* touches = event->targetTouches();
SliderThumbElement* thumb = ToSliderThumbElement(
GetTreeScope().getElementById(shadow_element_names::SliderThumb()));
if (!thumb || !touches)
return;
if (touches->length() == 1) {
if (event->type() == event_type_names::kTouchstart) {
start_point_ = touches->item(0)->AbsoluteLocation();
sliding_direction_ = kNoMove;
touch_started_ = true;
thumb->SetPositionFromPoint(touches->item(0)->AbsoluteLocation());
} else if (touch_started_) {
LayoutPoint current_point = touches->item(0)->AbsoluteLocation();
if (sliding_direction_ ==
kNoMove) { // Still needs to update the direction.
sliding_direction_ = GetDirection(current_point, start_point_);
}
// sliding_direction_ has been updated, so check whether it's okay to
// slide again.
if (CanSlide()) {
thumb->SetPositionFromPoint(touches->item(0)->AbsoluteLocation());
event->SetDefaultHandled();
}
}
}
}
SliderContainerElement::Direction SliderContainerElement::GetDirection(
LayoutPoint& point1,
LayoutPoint& point2) {
if (point1 == point2) {
return kNoMove;
}
if ((point1.X() - point2.X()).Abs() >= (point1.Y() - point2.Y()).Abs()) {
return kHorizontal;
}
return kVertical;
}
bool SliderContainerElement::CanSlide() {
if (!HostInput() || !HostInput()->GetLayoutObject() ||
!HostInput()->GetLayoutObject()->Style()) {
return false;
}
const ComputedStyle* slider_style = HostInput()->GetLayoutObject()->Style();
const TransformOperations& transforms = slider_style->Transform();
int transform_size = transforms.size();
if (transform_size > 0) {
for (int i = 0; i < transform_size; ++i) {
if (transforms.at(i)->GetType() == TransformOperation::kRotate) {
return true;
}
}
}
if ((sliding_direction_ == kVertical &&
slider_style->Appearance() == kSliderHorizontalPart) ||
(sliding_direction_ == kHorizontal &&
slider_style->Appearance() == kSliderVerticalPart)) {
return false;
}
return true;
}
const AtomicString& SliderContainerElement::ShadowPseudoId() const {
DEFINE_STATIC_LOCAL(const AtomicString, media_slider_container,
("-webkit-media-slider-container"));
DEFINE_STATIC_LOCAL(const AtomicString, slider_container,
("-webkit-slider-container"));
if (!OwnerShadowHost() || !OwnerShadowHost()->GetLayoutObject())
return slider_container;
const ComputedStyle& slider_style =
OwnerShadowHost()->GetLayoutObject()->StyleRef();
switch (slider_style.Appearance()) {
case kMediaSliderPart:
case kMediaSliderThumbPart:
case kMediaVolumeSliderPart:
case kMediaVolumeSliderThumbPart:
return media_slider_container;
default:
return slider_container;
}
}
void SliderContainerElement::UpdateTouchEventHandlerRegistry() {
if (has_touch_event_handler_) {
return;
}
if (GetDocument().GetPage() &&
GetDocument().Lifecycle().GetState() < DocumentLifecycle::kStopping) {
EventHandlerRegistry& registry =
GetDocument().GetFrame()->GetEventHandlerRegistry();
registry.DidAddEventHandler(
*this, EventHandlerRegistry::kTouchStartOrMoveEventPassive);
registry.DidAddEventHandler(*this, EventHandlerRegistry::kPointerEvent);
has_touch_event_handler_ = true;
}
}
void SliderContainerElement::DidMoveToNewDocument(Document& old_document) {
UpdateTouchEventHandlerRegistry();
HTMLElement::DidMoveToNewDocument(old_document);
}
void SliderContainerElement::RemoveAllEventListeners() {
Node::RemoveAllEventListeners();
has_touch_event_handler_ = false;
}
scoped_refptr<ComputedStyle>
SliderContainerElement::CustomStyleForLayoutObject() {
HTMLInputElement* input = HostInput();
DCHECK(input);
scoped_refptr<ComputedStyle> style = OriginalStyleForLayoutObject();
style->SetFlexDirection(HasVerticalAppearance(input) ? EFlexDirection::kColumn
: EFlexDirection::kRow);
return style;
}
} // namespace blink