/* * Copyright (C) 2010 Google Inc. All rights reserved. * Copyright (C) 2011-2018 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 "config.h" #include "RangeInputType.h" #include "AXObjectCache.h" #include "Decimal.h" #include "ElementChildIterator.h" #include "EventNames.h" #include "HTMLCollection.h" #include "HTMLInputElement.h" #include "HTMLParserIdioms.h" #include "InputTypeNames.h" #include "KeyboardEvent.h" #include "MouseEvent.h" #include "PlatformMouseEvent.h" #include "RenderSlider.h" #include "RuntimeEnabledFeatures.h" #include "ScopedEventQueue.h" #include "ShadowRoot.h" #include "SliderThumbElement.h" #include "StepRange.h" #include #include #if ENABLE(TOUCH_EVENTS) #include "Touch.h" #include "TouchEvent.h" #include "TouchList.h" #endif #if ENABLE(DATALIST_ELEMENT) #include "HTMLDataListElement.h" #include "HTMLOptionElement.h" #endif namespace WebCore { using namespace HTMLNames; static const int rangeDefaultMinimum = 0; static const int rangeDefaultMaximum = 100; static const int rangeDefaultStep = 1; static const int rangeDefaultStepBase = 0; static const int rangeStepScaleFactor = 1; static const StepRange::StepDescription rangeStepDescription { rangeDefaultStep, rangeDefaultStepBase, rangeStepScaleFactor }; static Decimal ensureMaximum(const Decimal& proposedValue, const Decimal& minimum, const Decimal& fallbackValue) { return proposedValue >= minimum ? proposedValue : std::max(minimum, fallbackValue); } RangeInputType::RangeInputType(HTMLInputElement& element) : InputType(Type::Range, element) { ASSERT(needsShadowSubtree()); } const AtomString& RangeInputType::formControlType() const { return InputTypeNames::range(); } double RangeInputType::valueAsDouble() const { ASSERT(element()); return parseToDoubleForNumberType(element()->value()); } ExceptionOr RangeInputType::setValueAsDecimal(const Decimal& newValue, TextFieldEventBehavior eventBehavior) const { ASSERT(element()); element()->setValue(serialize(newValue), eventBehavior); return { }; } bool RangeInputType::typeMismatchFor(const String& value) const { return !value.isEmpty() && !std::isfinite(parseToDoubleForNumberType(value)); } bool RangeInputType::supportsRequired() const { return false; } StepRange RangeInputType::createStepRange(AnyStepHandling anyStepHandling) const { ASSERT(element()); const Decimal minimum = parseToNumber(element()->attributeWithoutSynchronization(minAttr), rangeDefaultMinimum); const Decimal maximum = ensureMaximum(parseToNumber(element()->attributeWithoutSynchronization(maxAttr), rangeDefaultMaximum), minimum, rangeDefaultMaximum); const AtomString& precisionValue = element()->attributeWithoutSynchronization(precisionAttr); if (!precisionValue.isNull()) { const Decimal step = equalLettersIgnoringASCIICase(precisionValue, "float") ? Decimal::nan() : 1; return StepRange(minimum, RangeLimitations::Valid, minimum, maximum, step, rangeStepDescription); } const Decimal step = StepRange::parseStep(anyStepHandling, rangeStepDescription, element()->attributeWithoutSynchronization(stepAttr)); return StepRange(minimum, RangeLimitations::Valid, minimum, maximum, step, rangeStepDescription); } void RangeInputType::handleMouseDownEvent(MouseEvent& event) { ASSERT(element()); if (element()->isDisabledFormControl()) return; if (event.button() != LeftButton || !is(event.target())) return; ASSERT(element()->shadowRoot()); auto& targetNode = downcast(*event.target()); if (&targetNode != element() && !targetNode.isDescendantOf(element()->userAgentShadowRoot().get())) return; auto& thumb = typedSliderThumbElement(); if (&targetNode == &thumb) return; thumb.dragFrom(event.absoluteLocation()); } #if ENABLE(TOUCH_EVENTS) void RangeInputType::handleTouchEvent(TouchEvent& event) { #if PLATFORM(IOS_FAMILY) typedSliderThumbElement().handleTouchEvent(event); #elif ENABLE(TOUCH_SLIDER) ASSERT(element()); if (element()->isDisabledFormControl()) return; if (event.type() == eventNames().touchendEvent) { event.setDefaultHandled(); return; } RefPtr touches = event.targetTouches(); if (touches->length() == 1) { typedSliderThumbElement().setPositionFromPoint(touches->item(0)->absoluteLocation()); event.setDefaultHandled(); } #else UNUSED_PARAM(event); #endif } #if ENABLE(TOUCH_SLIDER) bool RangeInputType::hasTouchEventHandler() const { return true; } #endif #endif // ENABLE(TOUCH_EVENTS) void RangeInputType::disabledStateChanged() { typedSliderThumbElement().hostDisabledStateChanged(); } auto RangeInputType::handleKeydownEvent(KeyboardEvent& event) -> ShouldCallBaseEventHandler { ASSERT(element()); if (element()->isDisabledFormControl()) return ShouldCallBaseEventHandler::Yes; const String& key = event.keyIdentifier(); const Decimal current = parseToNumberOrNaN(element()->value()); ASSERT(current.isFinite()); auto stepRange = createStepRange(AnyStepHandling::Reject); // 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 = equalLettersIgnoringASCIICase(element()->attributeWithoutSynchronization(stepAttr), "any") ? (stepRange.maximum() - stepRange.minimum()) / 100 : stepRange.step(); const Decimal bigStep = std::max((stepRange.maximum() - stepRange.minimum()) / 10, step); bool isVertical = false; if (auto* renderer = element()->renderer()) { ControlPart part = renderer->style().appearance(); isVertical = part == SliderVerticalPart || part == MediaVolumeSliderPart; } Decimal newValue; if (key == "Up") newValue = current + step; else if (key == "Down") newValue = current - step; else if (key == "Left") newValue = isVertical ? current + step : current - step; else if (key == "Right") newValue = isVertical ? current - step : current + step; else if (key == "PageUp") newValue = current + bigStep; else if (key == "PageDown") newValue = current - bigStep; else if (key == "Home") newValue = isVertical ? stepRange.maximum() : stepRange.minimum(); else if (key == "End") newValue = isVertical ? stepRange.minimum() : stepRange.maximum(); else return ShouldCallBaseEventHandler::Yes; // Did not match any key binding. newValue = stepRange.clampValue(newValue); if (newValue != current) { EventQueueScope scope; setValueAsDecimal(newValue, DispatchInputAndChangeEvent); if (AXObjectCache* cache = element()->document().existingAXObjectCache()) cache->postNotification(element(), AXObjectCache::AXValueChanged); } event.setDefaultHandled(); return ShouldCallBaseEventHandler::Yes; } void RangeInputType::createShadowSubtreeAndUpdateInnerTextElementEditability(ContainerNode::ChildChange::Source source, bool) { ASSERT(needsShadowSubtree()); ASSERT(element()); ASSERT(element()->userAgentShadowRoot()); static MainThreadNeverDestroyed webkitSliderRunnableTrackName("-webkit-slider-runnable-track", AtomString::ConstructFromLiteral); Document& document = element()->document(); auto track = HTMLDivElement::create(document); track->setPseudo(webkitSliderRunnableTrackName); track->appendChild(source, SliderThumbElement::create(document)); auto container = SliderContainerElement::create(document); container->appendChild(source, track); element()->userAgentShadowRoot()->appendChild(source, container); } HTMLElement* RangeInputType::sliderTrackElement() const { ASSERT(element()); ASSERT(element()->userAgentShadowRoot()); ASSERT(element()->userAgentShadowRoot()->firstChild()); // container ASSERT(element()->userAgentShadowRoot()->firstChild()->isHTMLElement()); ASSERT(element()->userAgentShadowRoot()->firstChild()->firstChild()); // track RefPtr root = element()->userAgentShadowRoot(); if (!root) return nullptr; auto* container = childrenOfType(*root).first(); if (!container) return nullptr; return childrenOfType(*container).first(); } SliderThumbElement& RangeInputType::typedSliderThumbElement() const { ASSERT(sliderTrackElement()->firstChild()); // thumb ASSERT(sliderTrackElement()->firstChild()->isHTMLElement()); return static_cast(*sliderTrackElement()->firstChild()); } HTMLElement* RangeInputType::sliderThumbElement() const { return &typedSliderThumbElement(); } RenderPtr RangeInputType::createInputRenderer(RenderStyle&& style) { ASSERT(element()); return createRenderer(*element(), WTFMove(style)); } Decimal RangeInputType::parseToNumber(const String& src, const Decimal& defaultValue) const { return parseToDecimalForNumberType(src, defaultValue); } String RangeInputType::serialize(const Decimal& value) const { if (!value.isFinite()) return String(); return serializeForNumberType(value); } // FIXME: Could share this with BaseClickableWithKeyInputType and BaseCheckableInputType if we had a common base class. bool RangeInputType::accessKeyAction(bool sendMouseEvents) { auto* element = this->element(); return InputType::accessKeyAction(sendMouseEvents) || (element && element->dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents)); } void RangeInputType::attributeChanged(const QualifiedName& name) { // FIXME: Don't we need to do this work for precisionAttr too? if (name == maxAttr || name == minAttr || name == valueAttr) { // Sanitize the value. if (auto* element = this->element()) { if (element->hasDirtyValue()) element->setValue(element->value()); } typedSliderThumbElement().setPositionFromValue(); } InputType::attributeChanged(name); } void RangeInputType::setValue(const String& value, bool valueChanged, TextFieldEventBehavior eventBehavior) { InputType::setValue(value, valueChanged, eventBehavior); if (!valueChanged) return; if (eventBehavior == DispatchNoEvent) { ASSERT(element()); element()->setTextAsOfLastFormControlChangeEvent(value); } typedSliderThumbElement().setPositionFromValue(); } String RangeInputType::fallbackValue() const { return serializeForNumberType(createStepRange(AnyStepHandling::Reject).defaultValue()); } String RangeInputType::sanitizeValue(const String& proposedValue) const { StepRange stepRange(createStepRange(AnyStepHandling::Reject)); const Decimal proposedNumericValue = parseToNumber(proposedValue, stepRange.defaultValue()); return serializeForNumberType(stepRange.clampValue(proposedNumericValue)); } bool RangeInputType::shouldRespectListAttribute() { #if ENABLE(DATALIST_ELEMENT) return RuntimeEnabledFeatures::sharedFeatures().dataListElementEnabled(); #else return InputType::themeSupportsDataListUI(this); #endif } #if ENABLE(DATALIST_ELEMENT) void RangeInputType::dataListMayHaveChanged() { m_tickMarkValuesDirty = true; RefPtr sliderTrackElement = this->sliderTrackElement(); if (sliderTrackElement->renderer()) sliderTrackElement->renderer()->setNeedsLayout(); } void RangeInputType::updateTickMarkValues() { if (!m_tickMarkValuesDirty) return; m_tickMarkValues.clear(); m_tickMarkValuesDirty = false; ASSERT(element()); auto dataList = element()->dataList(); if (!dataList) return; Ref options = dataList->options(); m_tickMarkValues.reserveCapacity(options->length()); for (unsigned i = 0; i < options->length(); ++i) { RefPtr node = options->item(i); HTMLOptionElement& optionElement = downcast(*node); String optionValue = optionElement.value(); if (!element()->isValidValue(optionValue)) continue; m_tickMarkValues.append(parseToNumber(optionValue, Decimal::nan())); } m_tickMarkValues.shrinkToFit(); std::sort(m_tickMarkValues.begin(), m_tickMarkValues.end()); } std::optional RangeInputType::findClosestTickMarkValue(const Decimal& value) { updateTickMarkValues(); if (!m_tickMarkValues.size()) return std::nullopt; size_t left = 0; size_t right = m_tickMarkValues.size(); size_t middle; while (true) { ASSERT(left <= right); middle = left + (right - left) / 2; if (!middle) break; if (middle == m_tickMarkValues.size() - 1 && m_tickMarkValues[middle] < value) { middle++; break; } if (m_tickMarkValues[middle - 1] <= value && m_tickMarkValues[middle] >= value) break; if (m_tickMarkValues[middle] < value) left = middle; else right = middle; } std::optional closestLeft = middle ? std::make_optional(m_tickMarkValues[middle - 1]) : std::nullopt; std::optional closestRight = middle != m_tickMarkValues.size() ? std::make_optional(m_tickMarkValues[middle]) : std::nullopt; if (!closestLeft) return closestRight; if (!closestRight) return closestLeft; if (*closestRight - value < value - *closestLeft) return closestRight; return closestLeft; } #endif } // namespace WebCore