| /* |
| * Copyright (C) 2010 Google Inc. All rights reserved. |
| * Copyright (C) 2011 Apple 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/range_input_type.h" |
| |
| #include <algorithm> |
| #include <limits> |
| |
| #include "third_party/blink/renderer/core/accessibility/ax_object_cache.h" |
| #include "third_party/blink/renderer/core/dom/events/scoped_event_queue.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/keyboard_event.h" |
| #include "third_party/blink/renderer/core/events/mouse_event.h" |
| #include "third_party/blink/renderer/core/frame/use_counter.h" |
| #include "third_party/blink/renderer/core/html/forms/html_data_list_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_data_list_options_collection.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_option_element.h" |
| #include "third_party/blink/renderer/core/html/forms/slider_thumb_element.h" |
| #include "third_party/blink/renderer/core/html/forms/step_range.h" |
| #include "third_party/blink/renderer/core/html/html_div_element.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/html_names.h" |
| #include "third_party/blink/renderer/core/input_type_names.h" |
| #include "third_party/blink/renderer/core/layout/layout_slider.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/wtf/math_extras.h" |
| |
| namespace blink { |
| |
| using namespace html_names; |
| |
| static const int kRangeDefaultMinimum = 0; |
| static const int kRangeDefaultMaximum = 100; |
| static const int kRangeDefaultStep = 1; |
| static const int kRangeDefaultStepBase = 0; |
| static const int kRangeStepScaleFactor = 1; |
| |
| static Decimal EnsureMaximum(const Decimal& proposed_value, |
| const Decimal& minimum) { |
| return proposed_value >= minimum ? proposed_value : minimum; |
| } |
| |
| InputType* RangeInputType::Create(HTMLInputElement& element) { |
| return MakeGarbageCollected<RangeInputType>(element); |
| } |
| |
| RangeInputType::RangeInputType(HTMLInputElement& element) |
| : InputType(element), |
| InputTypeView(element), |
| tick_mark_values_dirty_(true) {} |
| |
| void RangeInputType::Trace(Visitor* visitor) { |
| InputTypeView::Trace(visitor); |
| InputType::Trace(visitor); |
| } |
| |
| InputTypeView* RangeInputType::CreateView() { |
| return this; |
| } |
| |
| InputType::ValueMode RangeInputType::GetValueMode() const { |
| return ValueMode::kValue; |
| } |
| |
| void RangeInputType::CountUsage() { |
| CountUsageIfVisible(WebFeature::kInputTypeRange); |
| if (const ComputedStyle* style = GetElement().GetComputedStyle()) { |
| if (style->Appearance() == kSliderVerticalPart) { |
| UseCounter::Count(GetElement().GetDocument(), |
| WebFeature::kInputTypeRangeVerticalAppearance); |
| } |
| } |
| } |
| |
| const AtomicString& RangeInputType::FormControlType() const { |
| return input_type_names::kRange; |
| } |
| |
| double RangeInputType::ValueAsDouble() const { |
| return ParseToDoubleForNumberType(GetElement().value()); |
| } |
| |
| void RangeInputType::SetValueAsDouble(double new_value, |
| TextFieldEventBehavior event_behavior, |
| ExceptionState& exception_state) const { |
| SetValueAsDecimal(Decimal::FromDouble(new_value), event_behavior, |
| exception_state); |
| } |
| |
| bool RangeInputType::TypeMismatchFor(const String& value) const { |
| return !value.IsEmpty() && !std::isfinite(ParseToDoubleForNumberType(value)); |
| } |
| |
| bool RangeInputType::SupportsRequired() const { |
| return false; |
| } |
| |
| StepRange RangeInputType::CreateStepRange( |
| AnyStepHandling any_step_handling) const { |
| DEFINE_STATIC_LOCAL( |
| const StepRange::StepDescription, step_description, |
| (kRangeDefaultStep, kRangeDefaultStepBase, kRangeStepScaleFactor)); |
| |
| const Decimal step_base = FindStepBase(kRangeDefaultStepBase); |
| const Decimal minimum = ParseToNumber(GetElement().FastGetAttribute(kMinAttr), |
| kRangeDefaultMinimum); |
| const Decimal maximum = |
| EnsureMaximum(ParseToNumber(GetElement().FastGetAttribute(kMaxAttr), |
| kRangeDefaultMaximum), |
| minimum); |
| |
| const Decimal step = |
| StepRange::ParseStep(any_step_handling, step_description, |
| GetElement().FastGetAttribute(kStepAttr)); |
| // Range type always has range limitations because it has default |
| // minimum/maximum. |
| // https://html.spec.whatwg.org/C/#range-state-(type=range):concept-input-min-default |
| const bool kHasRangeLimitations = true; |
| return StepRange(step_base, minimum, maximum, kHasRangeLimitations, step, |
| step_description); |
| } |
| |
| bool RangeInputType::IsSteppable() const { |
| return true; |
| } |
| |
| void RangeInputType::HandleMouseDownEvent(MouseEvent& event) { |
| if (GetElement().IsDisabledFormControl()) |
| return; |
| |
| Node* target_node = event.target()->ToNode(); |
| if (event.button() != |
| static_cast<int16_t>(WebPointerProperties::Button::kLeft) || |
| !target_node) |
| return; |
| DCHECK(IsShadowHost(GetElement())); |
| if (target_node != GetElement() && |
| !target_node->IsDescendantOf(GetElement().UserAgentShadowRoot())) |
| return; |
| SliderThumbElement* thumb = GetSliderThumbElement(); |
| if (target_node == thumb) |
| return; |
| thumb->DragFrom(LayoutPoint(event.AbsoluteLocation())); |
| } |
| |
| void RangeInputType::HandleKeydownEvent(KeyboardEvent& event) { |
| if (GetElement().IsDisabledFormControl()) |
| return; |
| |
| const String& key = event.key(); |
| |
| const Decimal current = ParseToNumberOrNaN(GetElement().value()); |
| DCHECK(current.IsFinite()); |
| |
| StepRange step_range(CreateStepRange(kRejectAny)); |
| |
| // FIXME: We can't use stepUp() for the step value "any". So, we increase |
| // or decrease the value by 1/100 of the value range. Is it reasonable? |
| const Decimal step = DeprecatedEqualIgnoringCase( |
| GetElement().FastGetAttribute(kStepAttr), "any") |
| ? (step_range.Maximum() - step_range.Minimum()) / 100 |
| : step_range.Step(); |
| const Decimal big_step = |
| std::max((step_range.Maximum() - step_range.Minimum()) / 10, step); |
| |
| TextDirection dir = TextDirection::kLtr; |
| bool is_vertical = false; |
| if (GetElement().GetLayoutObject()) { |
| dir = ComputedTextDirection(); |
| ControlPart part = GetElement().GetLayoutObject()->Style()->Appearance(); |
| is_vertical = part == kSliderVerticalPart; |
| } |
| |
| Decimal new_value; |
| if (key == "ArrowUp") { |
| new_value = current + step; |
| } else if (key == "ArrowDown") { |
| new_value = current - step; |
| } else if (key == "ArrowLeft") { |
| new_value = (is_vertical || dir == TextDirection::kRtl) ? current + step |
| : current - step; |
| } else if (key == "ArrowRight") { |
| new_value = (is_vertical || dir == TextDirection::kRtl) ? current - step |
| : current + step; |
| } else if (key == "PageUp") { |
| new_value = current + big_step; |
| } else if (key == "PageDown") { |
| new_value = current - big_step; |
| } else if (key == "Home") { |
| new_value = is_vertical ? step_range.Maximum() : step_range.Minimum(); |
| } else if (key == "End") { |
| new_value = is_vertical ? step_range.Minimum() : step_range.Maximum(); |
| } else { |
| return; // Did not match any key binding. |
| } |
| |
| new_value = step_range.ClampValue(new_value); |
| |
| if (new_value != current) { |
| EventQueueScope scope; |
| TextFieldEventBehavior event_behavior = |
| TextFieldEventBehavior::kDispatchInputAndChangeEvent; |
| SetValueAsDecimal(new_value, event_behavior, IGNORE_EXCEPTION_FOR_TESTING); |
| |
| if (AXObjectCache* cache = |
| GetElement().GetDocument().ExistingAXObjectCache()) |
| cache->HandleValueChanged(&GetElement()); |
| } |
| |
| event.SetDefaultHandled(); |
| } |
| |
| void RangeInputType::CreateShadowSubtree() { |
| DCHECK(IsShadowHost(GetElement())); |
| |
| Document& document = GetElement().GetDocument(); |
| auto* track = MakeGarbageCollected<HTMLDivElement>(document); |
| track->SetShadowPseudoId(AtomicString("-webkit-slider-runnable-track")); |
| track->setAttribute(kIdAttr, shadow_element_names::SliderTrack()); |
| track->AppendChild(SliderThumbElement::Create(document)); |
| auto* container = MakeGarbageCollected<SliderContainerElement>(document); |
| container->AppendChild(track); |
| GetElement().UserAgentShadowRoot()->AppendChild(container); |
| } |
| |
| LayoutObject* RangeInputType::CreateLayoutObject(const ComputedStyle&, |
| LegacyLayout) const { |
| return new LayoutSlider(&GetElement()); |
| } |
| |
| Decimal RangeInputType::ParseToNumber(const String& src, |
| const Decimal& default_value) const { |
| return ParseToDecimalForNumberType(src, default_value); |
| } |
| |
| String RangeInputType::Serialize(const Decimal& value) const { |
| if (!value.IsFinite()) |
| return String(); |
| return SerializeForNumberType(value); |
| } |
| |
| // FIXME: Could share this with KeyboardClickableInputTypeView and |
| // BaseCheckableInputType if we had a common base class. |
| void RangeInputType::AccessKeyAction(bool send_mouse_events) { |
| InputTypeView::AccessKeyAction(send_mouse_events); |
| |
| GetElement().DispatchSimulatedClick( |
| nullptr, send_mouse_events ? kSendMouseUpDownEvents : kSendNoEvents); |
| } |
| |
| void RangeInputType::SanitizeValueInResponseToMinOrMaxAttributeChange() { |
| if (GetElement().HasDirtyValue()) |
| GetElement().setValue(GetElement().value()); |
| else |
| GetElement().SetNonDirtyValue(GetElement().value()); |
| GetElement().UpdateView(); |
| } |
| |
| void RangeInputType::StepAttributeChanged() { |
| if (GetElement().HasDirtyValue()) |
| GetElement().setValue(GetElement().value()); |
| else |
| GetElement().SetNonDirtyValue(GetElement().value()); |
| GetElement().UpdateView(); |
| } |
| |
| void RangeInputType::DidSetValue(const String&, bool value_changed) { |
| if (value_changed) |
| GetElement().UpdateView(); |
| } |
| |
| void RangeInputType::UpdateView() { |
| GetSliderThumbElement()->SetPositionFromValue(); |
| } |
| |
| String RangeInputType::SanitizeValue(const String& proposed_value) const { |
| StepRange step_range(CreateStepRange(kRejectAny)); |
| const Decimal proposed_numeric_value = |
| ParseToNumber(proposed_value, step_range.DefaultValue()); |
| return SerializeForNumberType(step_range.ClampValue(proposed_numeric_value)); |
| } |
| |
| void RangeInputType::WarnIfValueIsInvalid(const String& value) const { |
| if (value.IsEmpty() || !GetElement().SanitizeValue(value).IsEmpty()) |
| return; |
| AddWarningToConsole( |
| "The specified value %s is not a valid number. The value must match to " |
| "the following regular expression: " |
| "-?(\\d+|\\d+\\.\\d+|\\.\\d+)([eE][-+]?\\d+)?", |
| value); |
| } |
| |
| void RangeInputType::DisabledAttributeChanged() { |
| if (GetElement().IsDisabledFormControl()) |
| GetSliderThumbElement()->StopDragging(); |
| } |
| |
| bool RangeInputType::ShouldRespectListAttribute() { |
| return true; |
| } |
| |
| inline SliderThumbElement* RangeInputType::GetSliderThumbElement() const { |
| return ToSliderThumbElementOrDie( |
| GetElement().UserAgentShadowRoot()->getElementById( |
| shadow_element_names::SliderThumb())); |
| } |
| |
| inline Element* RangeInputType::SliderTrackElement() const { |
| return GetElement().UserAgentShadowRoot()->getElementById( |
| shadow_element_names::SliderTrack()); |
| } |
| |
| void RangeInputType::ListAttributeTargetChanged() { |
| tick_mark_values_dirty_ = true; |
| if (auto* object = GetElement().GetLayoutObject()) |
| object->SetSubtreeShouldDoFullPaintInvalidation(); |
| Element* slider_track_element = SliderTrackElement(); |
| if (slider_track_element->GetLayoutObject()) { |
| slider_track_element->GetLayoutObject()->SetNeedsLayout( |
| layout_invalidation_reason::kAttributeChanged); |
| } |
| } |
| |
| static bool DecimalCompare(const Decimal& a, const Decimal& b) { |
| return a < b; |
| } |
| |
| void RangeInputType::UpdateTickMarkValues() { |
| if (!tick_mark_values_dirty_) |
| return; |
| tick_mark_values_.clear(); |
| tick_mark_values_dirty_ = false; |
| HTMLDataListElement* data_list = GetElement().DataList(); |
| if (!data_list) |
| return; |
| HTMLDataListOptionsCollection* options = data_list->options(); |
| tick_mark_values_.ReserveCapacity(options->length()); |
| for (unsigned i = 0; i < options->length(); ++i) { |
| HTMLOptionElement* option_element = options->Item(i); |
| String option_value = option_element->value(); |
| if (option_element->IsDisabledFormControl() || option_value.IsEmpty()) |
| continue; |
| if (!GetElement().IsValidValue(option_value)) |
| continue; |
| tick_mark_values_.push_back(ParseToNumber(option_value, Decimal::Nan())); |
| } |
| tick_mark_values_.ShrinkToFit(); |
| std::sort(tick_mark_values_.begin(), tick_mark_values_.end(), DecimalCompare); |
| } |
| |
| Decimal RangeInputType::FindClosestTickMarkValue(const Decimal& value) { |
| UpdateTickMarkValues(); |
| if (!tick_mark_values_.size()) |
| return Decimal::Nan(); |
| |
| wtf_size_t left = 0; |
| wtf_size_t right = tick_mark_values_.size(); |
| wtf_size_t middle; |
| while (true) { |
| DCHECK_LE(left, right); |
| middle = left + (right - left) / 2; |
| if (!middle) |
| break; |
| if (middle == tick_mark_values_.size() - 1 && |
| tick_mark_values_[middle] < value) { |
| middle++; |
| break; |
| } |
| if (tick_mark_values_[middle - 1] <= value && |
| tick_mark_values_[middle] >= value) |
| break; |
| |
| if (tick_mark_values_[middle] < value) |
| left = middle; |
| else |
| right = middle; |
| } |
| const Decimal closest_left = middle ? tick_mark_values_[middle - 1] |
| : Decimal::Infinity(Decimal::kNegative); |
| const Decimal closest_right = middle != tick_mark_values_.size() |
| ? tick_mark_values_[middle] |
| : Decimal::Infinity(Decimal::kPositive); |
| if (closest_right - value < value - closest_left) |
| return closest_right; |
| return closest_left; |
| } |
| |
| } // namespace blink |