1 | /* |
2 | * Copyright (C) 2010 Google Inc. All rights reserved. |
3 | * Copyright (C) 2011-2018 Apple 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 "config.h" |
33 | #include "RangeInputType.h" |
34 | |
35 | #include "AXObjectCache.h" |
36 | #include "ElementChildIterator.h" |
37 | #include "EventNames.h" |
38 | #include "HTMLInputElement.h" |
39 | #include "HTMLParserIdioms.h" |
40 | #include "InputTypeNames.h" |
41 | #include "KeyboardEvent.h" |
42 | #include "MouseEvent.h" |
43 | #include "PlatformMouseEvent.h" |
44 | #include "RenderSlider.h" |
45 | #include "RuntimeEnabledFeatures.h" |
46 | #include "ScopedEventQueue.h" |
47 | #include "ShadowRoot.h" |
48 | #include "SliderThumbElement.h" |
49 | #include <limits> |
50 | #include <wtf/MathExtras.h> |
51 | |
52 | #if ENABLE(TOUCH_EVENTS) |
53 | #include "Touch.h" |
54 | #include "TouchEvent.h" |
55 | #include "TouchList.h" |
56 | #endif |
57 | |
58 | #if ENABLE(DATALIST_ELEMENT) |
59 | #include "HTMLDataListElement.h" |
60 | #include "HTMLOptionElement.h" |
61 | #endif |
62 | |
63 | namespace WebCore { |
64 | |
65 | using namespace HTMLNames; |
66 | |
67 | static const int rangeDefaultMinimum = 0; |
68 | static const int rangeDefaultMaximum = 100; |
69 | static const int rangeDefaultStep = 1; |
70 | static const int rangeDefaultStepBase = 0; |
71 | static const int rangeStepScaleFactor = 1; |
72 | static const StepRange::StepDescription rangeStepDescription { rangeDefaultStep, rangeDefaultStepBase, rangeStepScaleFactor }; |
73 | |
74 | static Decimal ensureMaximum(const Decimal& proposedValue, const Decimal& minimum, const Decimal& fallbackValue) |
75 | { |
76 | return proposedValue >= minimum ? proposedValue : std::max(minimum, fallbackValue); |
77 | } |
78 | |
79 | RangeInputType::RangeInputType(HTMLInputElement& element) |
80 | : InputType(element) |
81 | { |
82 | } |
83 | |
84 | bool RangeInputType::isRangeControl() const |
85 | { |
86 | return true; |
87 | } |
88 | |
89 | const AtomicString& RangeInputType::formControlType() const |
90 | { |
91 | return InputTypeNames::range(); |
92 | } |
93 | |
94 | double RangeInputType::valueAsDouble() const |
95 | { |
96 | ASSERT(element()); |
97 | return parseToDoubleForNumberType(element()->value()); |
98 | } |
99 | |
100 | ExceptionOr<void> RangeInputType::setValueAsDecimal(const Decimal& newValue, TextFieldEventBehavior eventBehavior) const |
101 | { |
102 | ASSERT(element()); |
103 | element()->setValue(serialize(newValue), eventBehavior); |
104 | return { }; |
105 | } |
106 | |
107 | bool RangeInputType::typeMismatchFor(const String& value) const |
108 | { |
109 | return !value.isEmpty() && !std::isfinite(parseToDoubleForNumberType(value)); |
110 | } |
111 | |
112 | bool RangeInputType::supportsRequired() const |
113 | { |
114 | return false; |
115 | } |
116 | |
117 | StepRange RangeInputType::createStepRange(AnyStepHandling anyStepHandling) const |
118 | { |
119 | ASSERT(element()); |
120 | const Decimal minimum = parseToNumber(element()->attributeWithoutSynchronization(minAttr), rangeDefaultMinimum); |
121 | const Decimal maximum = ensureMaximum(parseToNumber(element()->attributeWithoutSynchronization(maxAttr), rangeDefaultMaximum), minimum, rangeDefaultMaximum); |
122 | |
123 | const AtomicString& precisionValue = element()->attributeWithoutSynchronization(precisionAttr); |
124 | if (!precisionValue.isNull()) { |
125 | const Decimal step = equalLettersIgnoringASCIICase(precisionValue, "float" ) ? Decimal::nan() : 1; |
126 | return StepRange(minimum, RangeLimitations::Valid, minimum, maximum, step, rangeStepDescription); |
127 | } |
128 | |
129 | const Decimal step = StepRange::parseStep(anyStepHandling, rangeStepDescription, element()->attributeWithoutSynchronization(stepAttr)); |
130 | return StepRange(minimum, RangeLimitations::Valid, minimum, maximum, step, rangeStepDescription); |
131 | } |
132 | |
133 | bool RangeInputType::isSteppable() const |
134 | { |
135 | return true; |
136 | } |
137 | |
138 | #if !PLATFORM(IOS_FAMILY) |
139 | |
140 | void RangeInputType::handleMouseDownEvent(MouseEvent& event) |
141 | { |
142 | ASSERT(element()); |
143 | if (element()->isDisabledFormControl()) |
144 | return; |
145 | |
146 | if (event.button() != LeftButton || !is<Node>(event.target())) |
147 | return; |
148 | ASSERT(element()->shadowRoot()); |
149 | auto& targetNode = downcast<Node>(*event.target()); |
150 | if (&targetNode != element() && !targetNode.isDescendantOf(element()->userAgentShadowRoot().get())) |
151 | return; |
152 | auto& thumb = typedSliderThumbElement(); |
153 | if (&targetNode == &thumb) |
154 | return; |
155 | thumb.dragFrom(event.absoluteLocation()); |
156 | } |
157 | |
158 | #endif |
159 | |
160 | #if ENABLE(TOUCH_EVENTS) |
161 | void RangeInputType::handleTouchEvent(TouchEvent& event) |
162 | { |
163 | #if PLATFORM(IOS_FAMILY) |
164 | typedSliderThumbElement().handleTouchEvent(event); |
165 | #elif ENABLE(TOUCH_SLIDER) |
166 | ASSERT(element()); |
167 | if (element()->isDisabledFormControl()) |
168 | return; |
169 | |
170 | if (event.type() == eventNames().touchendEvent) { |
171 | event.setDefaultHandled(); |
172 | return; |
173 | } |
174 | |
175 | RefPtr<TouchList> touches = event.targetTouches(); |
176 | if (touches->length() == 1) { |
177 | typedSliderThumbElement().setPositionFromPoint(touches->item(0)->absoluteLocation()); |
178 | event.setDefaultHandled(); |
179 | } |
180 | #else |
181 | UNUSED_PARAM(event); |
182 | #endif |
183 | } |
184 | |
185 | #if ENABLE(TOUCH_SLIDER) |
186 | bool RangeInputType::hasTouchEventHandler() const |
187 | { |
188 | return true; |
189 | } |
190 | #endif |
191 | #endif // ENABLE(TOUCH_EVENTS) |
192 | |
193 | void RangeInputType::disabledStateChanged() |
194 | { |
195 | typedSliderThumbElement().hostDisabledStateChanged(); |
196 | } |
197 | |
198 | auto RangeInputType::handleKeydownEvent(KeyboardEvent& event) -> ShouldCallBaseEventHandler |
199 | { |
200 | ASSERT(element()); |
201 | if (element()->isDisabledFormControl()) |
202 | return ShouldCallBaseEventHandler::Yes; |
203 | |
204 | const String& key = event.keyIdentifier(); |
205 | |
206 | const Decimal current = parseToNumberOrNaN(element()->value()); |
207 | ASSERT(current.isFinite()); |
208 | |
209 | StepRange stepRange(createStepRange(RejectAny)); |
210 | |
211 | // FIXME: We can't use stepUp() for the step value "any". So, we increase |
212 | // or decrease the value by 1/100 of the value range. Is it reasonable? |
213 | const Decimal step = equalLettersIgnoringASCIICase(element()->attributeWithoutSynchronization(stepAttr), "any" ) ? (stepRange.maximum() - stepRange.minimum()) / 100 : stepRange.step(); |
214 | const Decimal bigStep = std::max((stepRange.maximum() - stepRange.minimum()) / 10, step); |
215 | |
216 | bool isVertical = false; |
217 | if (auto* renderer = element()->renderer()) { |
218 | ControlPart part = renderer->style().appearance(); |
219 | isVertical = part == SliderVerticalPart || part == MediaVolumeSliderPart; |
220 | } |
221 | |
222 | Decimal newValue; |
223 | if (key == "Up" ) |
224 | newValue = current + step; |
225 | else if (key == "Down" ) |
226 | newValue = current - step; |
227 | else if (key == "Left" ) |
228 | newValue = isVertical ? current + step : current - step; |
229 | else if (key == "Right" ) |
230 | newValue = isVertical ? current - step : current + step; |
231 | else if (key == "PageUp" ) |
232 | newValue = current + bigStep; |
233 | else if (key == "PageDown" ) |
234 | newValue = current - bigStep; |
235 | else if (key == "Home" ) |
236 | newValue = isVertical ? stepRange.maximum() : stepRange.minimum(); |
237 | else if (key == "End" ) |
238 | newValue = isVertical ? stepRange.minimum() : stepRange.maximum(); |
239 | else |
240 | return ShouldCallBaseEventHandler::Yes; // Did not match any key binding. |
241 | |
242 | newValue = stepRange.clampValue(newValue); |
243 | |
244 | if (newValue != current) { |
245 | EventQueueScope scope; |
246 | setValueAsDecimal(newValue, DispatchInputAndChangeEvent); |
247 | |
248 | if (AXObjectCache* cache = element()->document().existingAXObjectCache()) |
249 | cache->postNotification(element(), AXObjectCache::AXValueChanged); |
250 | } |
251 | |
252 | event.setDefaultHandled(); |
253 | return ShouldCallBaseEventHandler::Yes; |
254 | } |
255 | |
256 | void RangeInputType::createShadowSubtree() |
257 | { |
258 | ASSERT(element()); |
259 | ASSERT(element()->userAgentShadowRoot()); |
260 | |
261 | Document& document = element()->document(); |
262 | auto track = HTMLDivElement::create(document); |
263 | track->setPseudo(AtomicString("-webkit-slider-runnable-track" , AtomicString::ConstructFromLiteral)); |
264 | track->appendChild(SliderThumbElement::create(document)); |
265 | auto container = SliderContainerElement::create(document); |
266 | container->appendChild(track); |
267 | element()->userAgentShadowRoot()->appendChild(container); |
268 | } |
269 | |
270 | HTMLElement* RangeInputType::sliderTrackElement() const |
271 | { |
272 | ASSERT(element()); |
273 | ASSERT(element()->userAgentShadowRoot()); |
274 | ASSERT(element()->userAgentShadowRoot()->firstChild()); // container |
275 | ASSERT(element()->userAgentShadowRoot()->firstChild()->isHTMLElement()); |
276 | ASSERT(element()->userAgentShadowRoot()->firstChild()->firstChild()); // track |
277 | |
278 | RefPtr<ShadowRoot> root = element()->userAgentShadowRoot(); |
279 | if (!root) |
280 | return nullptr; |
281 | |
282 | auto* container = childrenOfType<SliderContainerElement>(*root).first(); |
283 | if (!container) |
284 | return nullptr; |
285 | |
286 | return childrenOfType<HTMLElement>(*container).first(); |
287 | } |
288 | |
289 | SliderThumbElement& RangeInputType::typedSliderThumbElement() const |
290 | { |
291 | ASSERT(sliderTrackElement()->firstChild()); // thumb |
292 | ASSERT(sliderTrackElement()->firstChild()->isHTMLElement()); |
293 | |
294 | return static_cast<SliderThumbElement&>(*sliderTrackElement()->firstChild()); |
295 | } |
296 | |
297 | HTMLElement* RangeInputType::sliderThumbElement() const |
298 | { |
299 | return &typedSliderThumbElement(); |
300 | } |
301 | |
302 | RenderPtr<RenderElement> RangeInputType::createInputRenderer(RenderStyle&& style) |
303 | { |
304 | ASSERT(element()); |
305 | return createRenderer<RenderSlider>(*element(), WTFMove(style)); |
306 | } |
307 | |
308 | Decimal RangeInputType::parseToNumber(const String& src, const Decimal& defaultValue) const |
309 | { |
310 | return parseToDecimalForNumberType(src, defaultValue); |
311 | } |
312 | |
313 | String RangeInputType::serialize(const Decimal& value) const |
314 | { |
315 | if (!value.isFinite()) |
316 | return String(); |
317 | return serializeForNumberType(value); |
318 | } |
319 | |
320 | // FIXME: Could share this with BaseClickableWithKeyInputType and BaseCheckableInputType if we had a common base class. |
321 | void RangeInputType::accessKeyAction(bool sendMouseEvents) |
322 | { |
323 | InputType::accessKeyAction(sendMouseEvents); |
324 | |
325 | if (auto* element = this->element()) |
326 | element->dispatchSimulatedClick(0, sendMouseEvents ? SendMouseUpDownEvents : SendNoEvents); |
327 | } |
328 | |
329 | void RangeInputType::attributeChanged(const QualifiedName& name) |
330 | { |
331 | // FIXME: Don't we need to do this work for precisionAttr too? |
332 | if (name == maxAttr || name == minAttr) { |
333 | // Sanitize the value. |
334 | if (auto* element = this->element()) { |
335 | if (element->hasDirtyValue()) |
336 | element->setValue(element->value()); |
337 | } |
338 | typedSliderThumbElement().setPositionFromValue(); |
339 | } |
340 | InputType::attributeChanged(name); |
341 | } |
342 | |
343 | void RangeInputType::setValue(const String& value, bool valueChanged, TextFieldEventBehavior eventBehavior) |
344 | { |
345 | InputType::setValue(value, valueChanged, eventBehavior); |
346 | |
347 | if (!valueChanged) |
348 | return; |
349 | |
350 | if (eventBehavior == DispatchNoEvent) { |
351 | ASSERT(element()); |
352 | element()->setTextAsOfLastFormControlChangeEvent(value); |
353 | } |
354 | |
355 | typedSliderThumbElement().setPositionFromValue(); |
356 | } |
357 | |
358 | String RangeInputType::fallbackValue() const |
359 | { |
360 | return serializeForNumberType(createStepRange(RejectAny).defaultValue()); |
361 | } |
362 | |
363 | String RangeInputType::sanitizeValue(const String& proposedValue) const |
364 | { |
365 | StepRange stepRange(createStepRange(RejectAny)); |
366 | const Decimal proposedNumericValue = parseToNumber(proposedValue, stepRange.defaultValue()); |
367 | return serializeForNumberType(stepRange.clampValue(proposedNumericValue)); |
368 | } |
369 | |
370 | bool RangeInputType::shouldRespectListAttribute() |
371 | { |
372 | #if ENABLE(DATALIST_ELEMENT) |
373 | return RuntimeEnabledFeatures::sharedFeatures().dataListElementEnabled(); |
374 | #else |
375 | return InputType::themeSupportsDataListUI(this); |
376 | #endif |
377 | } |
378 | |
379 | #if ENABLE(DATALIST_ELEMENT) |
380 | void RangeInputType::listAttributeTargetChanged() |
381 | { |
382 | m_tickMarkValuesDirty = true; |
383 | RefPtr<HTMLElement> sliderTrackElement = this->sliderTrackElement(); |
384 | if (sliderTrackElement->renderer()) |
385 | sliderTrackElement->renderer()->setNeedsLayout(); |
386 | } |
387 | |
388 | void RangeInputType::updateTickMarkValues() |
389 | { |
390 | if (!m_tickMarkValuesDirty) |
391 | return; |
392 | m_tickMarkValues.clear(); |
393 | m_tickMarkValuesDirty = false; |
394 | ASSERT(element()); |
395 | auto dataList = element()->dataList(); |
396 | if (!dataList) |
397 | return; |
398 | Ref<HTMLCollection> options = dataList->options(); |
399 | m_tickMarkValues.reserveCapacity(options->length()); |
400 | for (unsigned i = 0; i < options->length(); ++i) { |
401 | RefPtr<Node> node = options->item(i); |
402 | HTMLOptionElement& optionElement = downcast<HTMLOptionElement>(*node); |
403 | String optionValue = optionElement.value(); |
404 | if (!element()->isValidValue(optionValue)) |
405 | continue; |
406 | m_tickMarkValues.append(parseToNumber(optionValue, Decimal::nan())); |
407 | } |
408 | m_tickMarkValues.shrinkToFit(); |
409 | std::sort(m_tickMarkValues.begin(), m_tickMarkValues.end()); |
410 | } |
411 | |
412 | Optional<Decimal> RangeInputType::findClosestTickMarkValue(const Decimal& value) |
413 | { |
414 | updateTickMarkValues(); |
415 | if (!m_tickMarkValues.size()) |
416 | return WTF::nullopt; |
417 | |
418 | size_t left = 0; |
419 | size_t right = m_tickMarkValues.size(); |
420 | size_t middle; |
421 | while (true) { |
422 | ASSERT(left <= right); |
423 | middle = left + (right - left) / 2; |
424 | if (!middle) |
425 | break; |
426 | if (middle == m_tickMarkValues.size() - 1 && m_tickMarkValues[middle] < value) { |
427 | middle++; |
428 | break; |
429 | } |
430 | if (m_tickMarkValues[middle - 1] <= value && m_tickMarkValues[middle] >= value) |
431 | break; |
432 | |
433 | if (m_tickMarkValues[middle] < value) |
434 | left = middle; |
435 | else |
436 | right = middle; |
437 | } |
438 | |
439 | Optional<Decimal> closestLeft = middle ? makeOptional(m_tickMarkValues[middle - 1]) : WTF::nullopt; |
440 | Optional<Decimal> closestRight = middle != m_tickMarkValues.size() ? makeOptional(m_tickMarkValues[middle]) : WTF::nullopt; |
441 | |
442 | if (!closestLeft) |
443 | return closestRight; |
444 | if (!closestRight) |
445 | return closestLeft; |
446 | |
447 | if (*closestRight - value < value - *closestLeft) |
448 | return closestRight; |
449 | |
450 | return closestLeft; |
451 | } |
452 | #endif |
453 | |
454 | } // namespace WebCore |
455 | |