1 /*
2  * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3  * Copyright (C) 2010 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 #include "third_party/blink/renderer/core/html/forms/slider_thumb_element.h"
33 
34 #include "third_party/blink/renderer/core/dom/events/event.h"
35 #include "third_party/blink/renderer/core/dom/node_computed_style.h"
36 #include "third_party/blink/renderer/core/dom/shadow_root.h"
37 #include "third_party/blink/renderer/core/events/mouse_event.h"
38 #include "third_party/blink/renderer/core/events/touch_event.h"
39 #include "third_party/blink/renderer/core/frame/event_handler_registry.h"
40 #include "third_party/blink/renderer/core/frame/local_frame.h"
41 #include "third_party/blink/renderer/core/html/forms/html_input_element.h"
42 #include "third_party/blink/renderer/core/html/forms/step_range.h"
43 #include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
44 #include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
45 #include "third_party/blink/renderer/core/input/event_handler.h"
46 #include "third_party/blink/renderer/core/layout/layout_block_flow.h"
47 #include "third_party/blink/renderer/core/layout/layout_object_factory.h"
48 #include "third_party/blink/renderer/core/layout/layout_theme.h"
49 #include "ui/base/ui_base_features.h"
50 
51 namespace blink {
52 
SliderThumbElement(Document & document)53 SliderThumbElement::SliderThumbElement(Document& document)
54     : HTMLDivElement(document), in_drag_mode_(false) {
55   SetHasCustomStyleCallbacks();
56   setAttribute(html_names::kIdAttr, shadow_element_names::kIdSliderThumb);
57 }
58 
SetPositionFromValue()59 void SliderThumbElement::SetPositionFromValue() {
60   // Since the code to calculate position is in the LayoutSliderThumb layout
61   // path, we don't actually update the value here. Instead, we poke at the
62   // layoutObject directly to trigger layout.
63   if (GetLayoutObject()) {
64     GetLayoutObject()->SetNeedsLayoutAndFullPaintInvalidation(
65         layout_invalidation_reason::kSliderValueChanged);
66     if (features::IsFormControlsRefreshEnabled()) {
67       HTMLInputElement* input(HostInput());
68       if (input && input->GetLayoutObject()) {
69         // the slider track selected value needs to be updated.
70         input->GetLayoutObject()->SetShouldDoFullPaintInvalidation();
71       }
72     }
73   }
74 }
75 
CreateLayoutObject(const ComputedStyle & style,LegacyLayout legacy)76 LayoutObject* SliderThumbElement::CreateLayoutObject(const ComputedStyle& style,
77                                                      LegacyLayout legacy) {
78   return LayoutObjectFactory::CreateBlockFlow(*this, style, legacy);
79 }
80 
IsDisabledFormControl() const81 bool SliderThumbElement::IsDisabledFormControl() const {
82   return HostInput() && HostInput()->IsDisabledFormControl();
83 }
84 
MatchesReadOnlyPseudoClass() const85 bool SliderThumbElement::MatchesReadOnlyPseudoClass() const {
86   return HostInput() && HostInput()->MatchesReadOnlyPseudoClass();
87 }
88 
MatchesReadWritePseudoClass() const89 bool SliderThumbElement::MatchesReadWritePseudoClass() const {
90   return HostInput() && HostInput()->MatchesReadWritePseudoClass();
91 }
92 
DragFrom(const LayoutPoint & point)93 void SliderThumbElement::DragFrom(const LayoutPoint& point) {
94   StartDragging();
95   SetPositionFromPoint(point);
96 }
97 
SetPositionFromPoint(const LayoutPoint & point)98 void SliderThumbElement::SetPositionFromPoint(const LayoutPoint& point) {
99   HTMLInputElement* input(HostInput());
100   Element* track_element = input->UserAgentShadowRoot()->getElementById(
101       shadow_element_names::kIdSliderTrack);
102 
103   const LayoutObject* input_object = input->GetLayoutObject();
104   const LayoutBox* thumb_box = GetLayoutBox();
105   const LayoutBox* track_box = track_element->GetLayoutBox();
106   if (!input_object || !thumb_box || !track_box)
107     return;
108 
109   PhysicalOffset point_in_track =
110       track_box->AbsoluteToLocalPoint(PhysicalOffsetToBeNoop(point));
111   const bool is_vertical = !thumb_box->StyleRef().IsHorizontalWritingMode();
112   bool is_left_to_right_direction =
113       thumb_box->StyleRef().IsLeftToRightDirection();
114   LayoutUnit track_size;
115   LayoutUnit position;
116   LayoutUnit current_position;
117   const auto* input_box = To<LayoutBox>(input_object);
118   PhysicalOffset thumb_offset =
119       thumb_box->LocalToAncestorPoint(PhysicalOffset(), input_box) -
120       track_box->LocalToAncestorPoint(PhysicalOffset(), input_box);
121   if (is_vertical) {
122     track_size = track_box->ContentHeight() - thumb_box->Size().Height();
123     position = point_in_track.top - thumb_box->Size().Height() / 2 -
124                thumb_box->MarginBottom();
125     current_position = thumb_offset.top;
126   } else {
127     track_size = track_box->ContentWidth() - thumb_box->Size().Width();
128     position = point_in_track.left - thumb_box->Size().Width() / 2;
129     position -= is_left_to_right_direction ? thumb_box->MarginLeft()
130                                            : thumb_box->MarginRight();
131     current_position = thumb_offset.left;
132   }
133   position = std::min(position, track_size).ClampNegativeToZero();
134   const Decimal ratio =
135       Decimal::FromDouble(static_cast<double>(position) / track_size);
136   const Decimal fraction =
137       is_vertical || !is_left_to_right_direction ? Decimal(1) - ratio : ratio;
138   StepRange step_range(input->CreateStepRange(kRejectAny));
139   Decimal value =
140       step_range.ClampValue(step_range.ValueFromProportion(fraction));
141 
142   Decimal closest = input->FindClosestTickMarkValue(value);
143   if (closest.IsFinite()) {
144     double closest_fraction =
145         step_range.ProportionFromValue(closest).ToDouble();
146     double closest_ratio = is_vertical || !is_left_to_right_direction
147                                ? 1.0 - closest_fraction
148                                : closest_fraction;
149     LayoutUnit closest_position(track_size * closest_ratio);
150     const LayoutUnit snapping_threshold(5);
151     if ((closest_position - position).Abs() <= snapping_threshold)
152       value = closest;
153   }
154 
155   String value_string = SerializeForNumberType(value);
156   if (value_string == input->value())
157     return;
158 
159   // FIXME: This is no longer being set from renderer. Consider updating the
160   // method name.
161   input->SetValueFromRenderer(value_string);
162   SetPositionFromValue();
163 }
164 
StartDragging()165 void SliderThumbElement::StartDragging() {
166   if (LocalFrame* frame = GetDocument().GetFrame()) {
167     // Note that we get to here only we through mouse event path. The touch
168     // events are implicitly captured to the starting element and will be
169     // handled in handleTouchEvent function.
170     frame->GetEventHandler().SetPointerCapture(PointerEventFactory::kMouseId,
171                                                this);
172     in_drag_mode_ = true;
173   }
174 }
175 
StopDragging()176 void SliderThumbElement::StopDragging() {
177   if (!in_drag_mode_)
178     return;
179 
180   if (LocalFrame* frame = GetDocument().GetFrame()) {
181     frame->GetEventHandler().ReleasePointerCapture(
182         PointerEventFactory::kMouseId, this);
183   }
184   in_drag_mode_ = false;
185   if (GetLayoutObject()) {
186     GetLayoutObject()->SetNeedsLayoutAndFullPaintInvalidation(
187         layout_invalidation_reason::kSliderValueChanged);
188   }
189   if (HostInput())
190     HostInput()->DispatchFormControlChangeEvent();
191 }
192 
DefaultEventHandler(Event & event)193 void SliderThumbElement::DefaultEventHandler(Event& event) {
194   if (IsA<PointerEvent>(event) &&
195       event.type() == event_type_names::kLostpointercapture) {
196     StopDragging();
197     return;
198   }
199 
200   if (!IsA<MouseEvent>(event)) {
201     HTMLDivElement::DefaultEventHandler(event);
202     return;
203   }
204 
205   // FIXME: Should handle this readonly/disabled check in more general way.
206   // Missing this kind of check is likely to occur elsewhere if adding it in
207   // each shadow element.
208   HTMLInputElement* input = HostInput();
209   if (!input || input->IsDisabledFormControl()) {
210     StopDragging();
211     HTMLDivElement::DefaultEventHandler(event);
212     return;
213   }
214 
215   auto& mouse_event = To<MouseEvent>(event);
216   bool is_left_button =
217       mouse_event.button() ==
218       static_cast<int16_t>(WebPointerProperties::Button::kLeft);
219   const AtomicString& event_type = event.type();
220 
221   // We intentionally do not call event->setDefaultHandled() here because
222   // MediaControlTimelineElement::defaultEventHandler() wants to handle these
223   // mouse events.
224   if (event_type == event_type_names::kMousedown && is_left_button) {
225     StartDragging();
226     return;
227   }
228   if (event_type == event_type_names::kMouseup && is_left_button) {
229     StopDragging();
230     return;
231   }
232   if (event_type == event_type_names::kMousemove) {
233     if (in_drag_mode_)
234       SetPositionFromPoint(LayoutPoint(mouse_event.AbsoluteLocation()));
235     return;
236   }
237 
238   HTMLDivElement::DefaultEventHandler(event);
239 }
240 
WillRespondToMouseMoveEvents()241 bool SliderThumbElement::WillRespondToMouseMoveEvents() {
242   const HTMLInputElement* input = HostInput();
243   if (input && !input->IsDisabledFormControl() && in_drag_mode_)
244     return true;
245 
246   return HTMLDivElement::WillRespondToMouseMoveEvents();
247 }
248 
WillRespondToMouseClickEvents()249 bool SliderThumbElement::WillRespondToMouseClickEvents() {
250   const HTMLInputElement* input = HostInput();
251   if (input && !input->IsDisabledFormControl())
252     return true;
253 
254   return HTMLDivElement::WillRespondToMouseClickEvents();
255 }
256 
DetachLayoutTree(bool performing_reattach)257 void SliderThumbElement::DetachLayoutTree(bool performing_reattach) {
258   if (in_drag_mode_) {
259     if (LocalFrame* frame = GetDocument().GetFrame()) {
260       frame->GetEventHandler().ReleasePointerCapture(
261           PointerEventFactory::kMouseId, this);
262     }
263   }
264   HTMLDivElement::DetachLayoutTree(performing_reattach);
265 }
266 
HostInput() const267 HTMLInputElement* SliderThumbElement::HostInput() const {
268   // Only HTMLInputElement creates SliderThumbElement instances as its shadow
269   // nodes.  So, ownerShadowHost() must be an HTMLInputElement.
270   return To<HTMLInputElement>(OwnerShadowHost());
271 }
272 
ShadowPseudoId() const273 const AtomicString& SliderThumbElement::ShadowPseudoId() const {
274   HTMLInputElement* input = HostInput();
275   if (!input || !input->GetLayoutObject())
276     return shadow_element_names::kPseudoSliderThumb;
277 
278   const ComputedStyle& slider_style = input->GetLayoutObject()->StyleRef();
279   switch (slider_style.EffectiveAppearance()) {
280     case kMediaSliderPart:
281     case kMediaSliderThumbPart:
282     case kMediaVolumeSliderPart:
283     case kMediaVolumeSliderThumbPart:
284       return shadow_element_names::kPseudoMediaSliderThumb;
285     default:
286       return shadow_element_names::kPseudoSliderThumb;
287   }
288 }
289 
CustomStyleForLayoutObject()290 scoped_refptr<ComputedStyle> SliderThumbElement::CustomStyleForLayoutObject() {
291   Element* host = OwnerShadowHost();
292   DCHECK(host);
293   const ComputedStyle& host_style = host->ComputedStyleRef();
294   scoped_refptr<ComputedStyle> style = OriginalStyleForLayoutObject();
295 
296   if (host_style.EffectiveAppearance() == kSliderVerticalPart)
297     style->SetEffectiveAppearance(kSliderThumbVerticalPart);
298   else if (host_style.EffectiveAppearance() == kSliderHorizontalPart)
299     style->SetEffectiveAppearance(kSliderThumbHorizontalPart);
300   else if (host_style.EffectiveAppearance() == kMediaSliderPart)
301     style->SetEffectiveAppearance(kMediaSliderThumbPart);
302   else if (host_style.EffectiveAppearance() == kMediaVolumeSliderPart)
303     style->SetEffectiveAppearance(kMediaVolumeSliderThumbPart);
304   if (style->HasEffectiveAppearance())
305     LayoutTheme::GetTheme().AdjustSliderThumbSize(*style);
306 
307   return style;
308 }
309 
310 // --------------------------------
311 
SliderContainerElement(Document & document)312 SliderContainerElement::SliderContainerElement(Document& document)
313     : HTMLDivElement(document),
314       has_touch_event_handler_(false),
315       touch_started_(false),
316       sliding_direction_(kNoMove) {
317   UpdateTouchEventHandlerRegistry();
318   SetHasCustomStyleCallbacks();
319 }
320 
HostInput() const321 HTMLInputElement* SliderContainerElement::HostInput() const {
322   return To<HTMLInputElement>(OwnerShadowHost());
323 }
324 
CreateLayoutObject(const ComputedStyle & style,LegacyLayout legacy)325 LayoutObject* SliderContainerElement::CreateLayoutObject(
326     const ComputedStyle& style,
327     LegacyLayout legacy) {
328   return LayoutObjectFactory::CreateFlexibleBox(*this, style, legacy);
329 }
330 
DefaultEventHandler(Event & event)331 void SliderContainerElement::DefaultEventHandler(Event& event) {
332   if (auto* touch_event = DynamicTo<TouchEvent>(event)) {
333     HandleTouchEvent(touch_event);
334     return;
335   }
336 }
337 
HandleTouchEvent(TouchEvent * event)338 void SliderContainerElement::HandleTouchEvent(TouchEvent* event) {
339   HTMLInputElement* input = HostInput();
340   if (!input || input->IsDisabledFormControl() || !event)
341     return;
342 
343   if (event->type() == event_type_names::kTouchend) {
344     // TODO: Also do this for touchcancel?
345     input->DispatchFormControlChangeEvent();
346     event->SetDefaultHandled();
347     sliding_direction_ = kNoMove;
348     touch_started_ = false;
349     return;
350   }
351 
352   // The direction of this series of touch actions has been determined, which is
353   // perpendicular to the slider, so no need to adjust the value.
354   if (!CanSlide()) {
355     return;
356   }
357 
358   TouchList* touches = event->targetTouches();
359   auto* thumb = To<SliderThumbElement>(
360       GetTreeScope().getElementById(shadow_element_names::kIdSliderThumb));
361   if (!thumb || !touches)
362     return;
363 
364   if (touches->length() == 1) {
365     if (event->type() == event_type_names::kTouchstart) {
366       start_point_ = touches->item(0)->AbsoluteLocation();
367       sliding_direction_ = kNoMove;
368       touch_started_ = true;
369       thumb->SetPositionFromPoint(touches->item(0)->AbsoluteLocation());
370     } else if (touch_started_) {
371       LayoutPoint current_point = touches->item(0)->AbsoluteLocation();
372       if (sliding_direction_ ==
373           kNoMove) {  // Still needs to update the direction.
374         sliding_direction_ = GetDirection(current_point, start_point_);
375       }
376 
377       // sliding_direction_ has been updated, so check whether it's okay to
378       // slide again.
379       if (CanSlide()) {
380         thumb->SetPositionFromPoint(touches->item(0)->AbsoluteLocation());
381         event->SetDefaultHandled();
382       }
383     }
384   }
385 }
386 
GetDirection(LayoutPoint & point1,LayoutPoint & point2)387 SliderContainerElement::Direction SliderContainerElement::GetDirection(
388     LayoutPoint& point1,
389     LayoutPoint& point2) {
390   if (point1 == point2) {
391     return kNoMove;
392   }
393   if ((point1.X() - point2.X()).Abs() >= (point1.Y() - point2.Y()).Abs()) {
394     return kHorizontal;
395   }
396   return kVertical;
397 }
398 
CanSlide()399 bool SliderContainerElement::CanSlide() {
400   if (!HostInput() || !HostInput()->GetLayoutObject() ||
401       !HostInput()->GetLayoutObject()->Style()) {
402     return false;
403   }
404   const ComputedStyle* slider_style = HostInput()->GetLayoutObject()->Style();
405   const TransformOperations& transforms = slider_style->Transform();
406   int transform_size = transforms.size();
407   if (transform_size > 0) {
408     for (int i = 0; i < transform_size; ++i) {
409       if (transforms.at(i)->GetType() == TransformOperation::kRotate) {
410         return true;
411       }
412     }
413   }
414   bool is_horizontal = GetComputedStyle()->IsHorizontalWritingMode();
415   if ((sliding_direction_ == kVertical && is_horizontal) ||
416       (sliding_direction_ == kHorizontal && !is_horizontal)) {
417     return false;
418   }
419   return true;
420 }
421 
ShadowPseudoId() const422 const AtomicString& SliderContainerElement::ShadowPseudoId() const {
423   if (!OwnerShadowHost() || !OwnerShadowHost()->GetLayoutObject())
424     return shadow_element_names::kPseudoSliderContainer;
425 
426   const ComputedStyle& slider_style =
427       OwnerShadowHost()->GetLayoutObject()->StyleRef();
428   switch (slider_style.EffectiveAppearance()) {
429     case kMediaSliderPart:
430     case kMediaSliderThumbPart:
431     case kMediaVolumeSliderPart:
432     case kMediaVolumeSliderThumbPart:
433       return shadow_element_names::kPseudoMediaSliderContainer;
434     default:
435       return shadow_element_names::kPseudoSliderContainer;
436   }
437 }
438 
UpdateTouchEventHandlerRegistry()439 void SliderContainerElement::UpdateTouchEventHandlerRegistry() {
440   if (has_touch_event_handler_) {
441     return;
442   }
443   if (GetDocument().GetPage() &&
444       GetDocument().Lifecycle().GetState() < DocumentLifecycle::kStopping) {
445     EventHandlerRegistry& registry =
446         GetDocument().GetFrame()->GetEventHandlerRegistry();
447     registry.DidAddEventHandler(
448         *this, EventHandlerRegistry::kTouchStartOrMoveEventPassive);
449     registry.DidAddEventHandler(*this, EventHandlerRegistry::kPointerEvent);
450     has_touch_event_handler_ = true;
451   }
452 }
453 
DidMoveToNewDocument(Document & old_document)454 void SliderContainerElement::DidMoveToNewDocument(Document& old_document) {
455   UpdateTouchEventHandlerRegistry();
456   HTMLElement::DidMoveToNewDocument(old_document);
457 }
458 
RemoveAllEventListeners()459 void SliderContainerElement::RemoveAllEventListeners() {
460   Node::RemoveAllEventListeners();
461   has_touch_event_handler_ = false;
462 }
463 
464 }  // namespace blink
465