mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 06:58:11 +00:00
LibWeb: Add basic input range rendering
This commit is contained in:
parent
d28c400fcb
commit
0f37e0ee89
7 changed files with 169 additions and 3 deletions
68
Base/res/html/misc/input-range.html
Normal file
68
Base/res/html/misc/input-range.html
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Input range showcase</title>
|
||||||
|
<style>
|
||||||
|
input[type=range] {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 8.4px;
|
||||||
|
cursor: drag;
|
||||||
|
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||||
|
background: #3071a9;
|
||||||
|
border-radius: 1.3px;
|
||||||
|
border: 0.2px solid #010101;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy::-webkit-slider-thumb {
|
||||||
|
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
height: 36px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin-top: -14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy:focus::-webkit-slider-runnable-track {
|
||||||
|
background: #367ebd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Input range showcase</h1>
|
||||||
|
<p>
|
||||||
|
<input type="range" oninput="document.getElementById('a-value').textContent = this.value">
|
||||||
|
Value: <span id="a-value">?</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input type="range" value="0" class="fancy" oninput="document.getElementById('b-value').textContent = this.value">
|
||||||
|
Value: <span id="b-value">?</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input type="range" min="10" value="11" max="15" oninput="document.getElementById('c-value').textContent = this.value">
|
||||||
|
Value: <span id="c-value">?</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input type="range" class="fancy" min="50" max="200" step="5"
|
||||||
|
oninput="document.getElementById('d-value').textContent = this.value">
|
||||||
|
Value: <span id="d-value">?</span>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -29,7 +29,7 @@
|
||||||
<input type="time" id="time" value="time" /><br />
|
<input type="time" id="time" value="time" /><br />
|
||||||
<input type="datetime-local" id="datetime-local" value="datetime-local" /><br />
|
<input type="datetime-local" id="datetime-local" value="datetime-local" /><br />
|
||||||
<input type="number" id="number" value="number" /><br />
|
<input type="number" id="number" value="number" /><br />
|
||||||
<input type="range" id="range" value="range" /><br />
|
<input type="range" id="range" value="25" /><br />
|
||||||
<input type="color" id="color" value="color" /><br />
|
<input type="color" id="color" value="color" /><br />
|
||||||
<input type="checkbox" id="checkbox" value="checkbox" /><br />
|
<input type="checkbox" id="checkbox" value="checkbox" /><br />
|
||||||
<input type="radio" id="radio-a" value="a" name="test-radio" /><br />
|
<input type="radio" id="radio-a" value="a" name="test-radio" /><br />
|
||||||
|
|
|
@ -26,7 +26,7 @@ label {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FIXME: This is a temporary hack until we can render a native-looking frame for these. */
|
/* FIXME: This is a temporary hack until we can render a native-looking frame for these. */
|
||||||
input:not([type=submit], input[type=button], input[type=reset], input[type=color], input[type=checkbox], input[type=radio]), textarea {
|
input:not([type=submit], input[type=button], input[type=reset], input[type=color], input[type=checkbox], input[type=radio], input[type=range]), textarea {
|
||||||
border: 1px solid ButtonBorder;
|
border: 1px solid ButtonBorder;
|
||||||
min-height: 16px;
|
min-height: 16px;
|
||||||
width: attr(size ch, 20ch);
|
width: attr(size ch, 20ch);
|
||||||
|
@ -70,6 +70,30 @@ option {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom <input type="range"> styles */
|
||||||
|
input[type=range] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20ch;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-runnable-track, input[type=range]::-webkit-slider-thumb {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-runnable-track {
|
||||||
|
height: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
background-color: hsl(217, 71%, 53%);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
margin-top: -6px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: hsl(0, 0%, 96%);
|
||||||
|
outline: 1px solid rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom <meter> styles */
|
/* Custom <meter> styles */
|
||||||
meter {
|
meter {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -390,6 +390,10 @@ StringView Selector::PseudoElement::name(Selector::PseudoElement::Type pseudo_el
|
||||||
return "placeholder"sv;
|
return "placeholder"sv;
|
||||||
case Selector::PseudoElement::Type::Selection:
|
case Selector::PseudoElement::Type::Selection:
|
||||||
return "selection"sv;
|
return "selection"sv;
|
||||||
|
case Selector::PseudoElement::Type::SliderRunnableTrack:
|
||||||
|
return "-webkit-slider-runnable-track"sv;
|
||||||
|
case Selector::PseudoElement::Type::SliderThumb:
|
||||||
|
return "-webkit-slider-thumb"sv;
|
||||||
case Selector::PseudoElement::Type::KnownPseudoElementCount:
|
case Selector::PseudoElement::Type::KnownPseudoElementCount:
|
||||||
break;
|
break;
|
||||||
case Selector::PseudoElement::Type::UnknownWebKit:
|
case Selector::PseudoElement::Type::UnknownWebKit:
|
||||||
|
@ -426,6 +430,10 @@ Optional<Selector::PseudoElement> Selector::PseudoElement::from_string(FlyString
|
||||||
return Selector::PseudoElement { Selector::PseudoElement::Type::Placeholder };
|
return Selector::PseudoElement { Selector::PseudoElement::Type::Placeholder };
|
||||||
} else if (name.equals_ignoring_ascii_case("selection"sv)) {
|
} else if (name.equals_ignoring_ascii_case("selection"sv)) {
|
||||||
return Selector::PseudoElement { Selector::PseudoElement::Type::Selection };
|
return Selector::PseudoElement { Selector::PseudoElement::Type::Selection };
|
||||||
|
} else if (name.equals_ignoring_ascii_case("-webkit-slider-runnable-track"sv)) {
|
||||||
|
return Selector::PseudoElement { Selector::PseudoElement::Type::SliderRunnableTrack };
|
||||||
|
} else if (name.equals_ignoring_ascii_case("-webkit-slider-thumb"sv)) {
|
||||||
|
return Selector::PseudoElement { Selector::PseudoElement::Type::SliderThumb };
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,8 @@ public:
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
Placeholder,
|
Placeholder,
|
||||||
Selection,
|
Selection,
|
||||||
|
SliderRunnableTrack,
|
||||||
|
SliderThumb,
|
||||||
|
|
||||||
// Keep this last.
|
// Keep this last.
|
||||||
KnownPseudoElementCount,
|
KnownPseudoElementCount,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
|
* Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
|
||||||
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
|
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
|
||||||
* Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
|
* Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
|
||||||
|
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -62,6 +63,7 @@ void HTMLInputElement::visit_edges(Cell::Visitor& visitor)
|
||||||
visitor.visit(m_color_well_element);
|
visitor.visit(m_color_well_element);
|
||||||
visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group);
|
visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group);
|
||||||
visitor.visit(m_selected_files);
|
visitor.visit(m_selected_files);
|
||||||
|
visitor.visit(m_slider_thumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
JS::GCPtr<Layout::Node> HTMLInputElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
|
JS::GCPtr<Layout::Node> HTMLInputElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
|
||||||
|
@ -547,6 +549,9 @@ void HTMLInputElement::create_shadow_tree_if_needed()
|
||||||
case TypeAttributeState::Color:
|
case TypeAttributeState::Color:
|
||||||
create_color_input_shadow_tree();
|
create_color_input_shadow_tree();
|
||||||
break;
|
break;
|
||||||
|
case TypeAttributeState::Range:
|
||||||
|
create_range_input_shadow_tree();
|
||||||
|
break;
|
||||||
// FIXME: This could be better factored. Everything except the above types becomes a text input.
|
// FIXME: This could be better factored. Everything except the above types becomes a text input.
|
||||||
default:
|
default:
|
||||||
create_text_input_shadow_tree();
|
create_text_input_shadow_tree();
|
||||||
|
@ -674,6 +679,38 @@ void HTMLInputElement::create_color_input_shadow_tree()
|
||||||
set_shadow_root(shadow_root);
|
set_shadow_root(shadow_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HTMLInputElement::create_range_input_shadow_tree()
|
||||||
|
{
|
||||||
|
auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed);
|
||||||
|
set_shadow_root(shadow_root);
|
||||||
|
|
||||||
|
auto slider_runnable_track = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
|
||||||
|
slider_runnable_track->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::SliderRunnableTrack);
|
||||||
|
MUST(shadow_root->append_child(slider_runnable_track));
|
||||||
|
|
||||||
|
m_slider_thumb = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML));
|
||||||
|
m_slider_thumb->set_use_pseudo_element(CSS::Selector::PseudoElement::Type::SliderThumb);
|
||||||
|
MUST(slider_runnable_track->append_child(*m_slider_thumb));
|
||||||
|
update_slider_thumb_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTMLInputElement::update_slider_thumb_element()
|
||||||
|
{
|
||||||
|
double minimum = *min();
|
||||||
|
double maximum = *max();
|
||||||
|
|
||||||
|
double default_value = minimum + (maximum - minimum) / 2;
|
||||||
|
if (maximum < minimum)
|
||||||
|
default_value = minimum;
|
||||||
|
|
||||||
|
double value = MUST(value_as_number());
|
||||||
|
if (!isfinite(value))
|
||||||
|
value = default_value;
|
||||||
|
|
||||||
|
double position = (value - minimum) / (maximum - minimum) * 100;
|
||||||
|
MUST(m_slider_thumb->style_for_bindings()->set_property(CSS::PropertyID::MarginLeft, MUST(String::formatted("{}%", position))));
|
||||||
|
}
|
||||||
|
|
||||||
void HTMLInputElement::did_receive_focus()
|
void HTMLInputElement::did_receive_focus()
|
||||||
{
|
{
|
||||||
auto* browsing_context = document().browsing_context();
|
auto* browsing_context = document().browsing_context();
|
||||||
|
@ -714,6 +751,9 @@ void HTMLInputElement::attribute_changed(FlyString const& name, Optional<String>
|
||||||
|
|
||||||
if (type_state() == TypeAttributeState::Color && m_color_well_element)
|
if (type_state() == TypeAttributeState::Color && m_color_well_element)
|
||||||
MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
|
MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
|
||||||
|
|
||||||
|
if (type_state() == TypeAttributeState::Range && m_slider_thumb)
|
||||||
|
update_slider_thumb_element();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!m_dirty_value) {
|
if (!m_dirty_value) {
|
||||||
|
@ -722,6 +762,9 @@ void HTMLInputElement::attribute_changed(FlyString const& name, Optional<String>
|
||||||
|
|
||||||
if (type_state() == TypeAttributeState::Color && m_color_well_element)
|
if (type_state() == TypeAttributeState::Color && m_color_well_element)
|
||||||
MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
|
MUST(m_color_well_element->style_for_bindings()->set_property(CSS::PropertyID::BackgroundColor, m_value));
|
||||||
|
|
||||||
|
if (type_state() == TypeAttributeState::Range && m_slider_thumb)
|
||||||
|
update_slider_thumb_element();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (name == HTML::AttributeNames::placeholder) {
|
} else if (name == HTML::AttributeNames::placeholder) {
|
||||||
|
@ -1051,6 +1094,10 @@ Optional<double> HTMLInputElement::convert_string_to_number(StringView input) co
|
||||||
if (type_state() == TypeAttributeState::Number)
|
if (type_state() == TypeAttributeState::Number)
|
||||||
return parse_floating_point_number(input);
|
return parse_floating_point_number(input);
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-string-number
|
||||||
|
if (type_state() == TypeAttributeState::Range)
|
||||||
|
return parse_floating_point_number(input);
|
||||||
|
|
||||||
dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type());
|
dbgln("HTMLInputElement::convert_string_to_number() not implemented for input type {}", type());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -1062,6 +1109,10 @@ String HTMLInputElement::covert_number_to_string(double input) const
|
||||||
if (type_state() == TypeAttributeState::Number)
|
if (type_state() == TypeAttributeState::Number)
|
||||||
return MUST(String::number(input));
|
return MUST(String::number(input));
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-number-string
|
||||||
|
if (type_state() == TypeAttributeState::Range)
|
||||||
|
return MUST(String::number(input));
|
||||||
|
|
||||||
dbgln("HTMLInputElement::covert_number_to_string() not implemented for input type {}", type());
|
dbgln("HTMLInputElement::covert_number_to_string() not implemented for input type {}", type());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -1109,6 +1160,10 @@ double HTMLInputElement::default_step() const
|
||||||
if (type_state() == TypeAttributeState::Number)
|
if (type_state() == TypeAttributeState::Number)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-step-default
|
||||||
|
if (type_state() == TypeAttributeState::Range)
|
||||||
|
return 1;
|
||||||
|
|
||||||
dbgln("HTMLInputElement::default_step() not implemented for input type {}", type());
|
dbgln("HTMLInputElement::default_step() not implemented for input type {}", type());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1116,10 +1171,14 @@ double HTMLInputElement::default_step() const
|
||||||
// https://html.spec.whatwg.org/multipage/input.html#concept-input-step-scale
|
// https://html.spec.whatwg.org/multipage/input.html#concept-input-step-scale
|
||||||
double HTMLInputElement::step_scale_factor() const
|
double HTMLInputElement::step_scale_factor() const
|
||||||
{
|
{
|
||||||
// https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-step-default
|
// https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-step-scale
|
||||||
if (type_state() == TypeAttributeState::Number)
|
if (type_state() == TypeAttributeState::Number)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
|
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-step-scale
|
||||||
|
if (type_state() == TypeAttributeState::Range)
|
||||||
|
return 1;
|
||||||
|
|
||||||
dbgln("HTMLInputElement::step_scale_factor() not implemented for input type {}", type());
|
dbgln("HTMLInputElement::step_scale_factor() not implemented for input type {}", type());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
|
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
|
||||||
* Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
|
* Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
|
||||||
|
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -202,6 +203,7 @@ private:
|
||||||
void create_shadow_tree_if_needed();
|
void create_shadow_tree_if_needed();
|
||||||
void create_text_input_shadow_tree();
|
void create_text_input_shadow_tree();
|
||||||
void create_color_input_shadow_tree();
|
void create_color_input_shadow_tree();
|
||||||
|
void create_range_input_shadow_tree();
|
||||||
WebIDL::ExceptionOr<void> run_input_activation_behavior();
|
WebIDL::ExceptionOr<void> run_input_activation_behavior();
|
||||||
void set_checked_within_group();
|
void set_checked_within_group();
|
||||||
|
|
||||||
|
@ -219,6 +221,9 @@ private:
|
||||||
JS::GCPtr<DOM::Text> m_text_node;
|
JS::GCPtr<DOM::Text> m_text_node;
|
||||||
bool m_checked { false };
|
bool m_checked { false };
|
||||||
|
|
||||||
|
void update_slider_thumb_element();
|
||||||
|
JS::GCPtr<DOM::Element> m_slider_thumb;
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate
|
// https://html.spec.whatwg.org/multipage/input.html#dom-input-indeterminate
|
||||||
bool m_indeterminate { false };
|
bool m_indeterminate { false };
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue