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