mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 13:32:45 +00:00 
			
		
		
		
	LibWeb: Create a shadow tree for <input type=file> elements
This creates a button to prompt users to select a file, and a label to show information about the selected file(s). Clicking either shadow element will activate the input element.
This commit is contained in:
		
							parent
							
								
									8319c7cfb8
								
							
						
					
					
						commit
						435c2c24d1
					
				
					 6 changed files with 119 additions and 3 deletions
				
			
		
							
								
								
									
										54
									
								
								Tests/LibWeb/Layout/expected/input-file.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								Tests/LibWeb/Layout/expected/input-file.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | ||||||
|  | Viewport <#document> at (0,0) content-size 800x600 children: not-inline | ||||||
|  |   BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline | ||||||
|  |     BlockContainer <body> at (8,8) content-size 784x21 children: inline | ||||||
|  |       frag 0 from BlockContainer start: 0, length: 0, rect: [8,8 236.65625x21] baseline: 13.296875 | ||||||
|  |       frag 1 from TextNode start: 0, length: 1, rect: [245,8 8x17] baseline: 13.296875 | ||||||
|  |           " " | ||||||
|  |       frag 2 from BlockContainer start: 0, length: 0, rect: [253,8 255.34375x21] baseline: 13.296875 | ||||||
|  |       BlockContainer <input> at (8,8) content-size 236.65625x21 inline-block [BFC] children: inline | ||||||
|  |         frag 0 from BlockContainer start: 0, length: 0, rect: [13,10 94.375x17] baseline: 13.296875 | ||||||
|  |         frag 1 from Label start: 0, length: 0, rect: [116,8 128.28125x17] baseline: 13.296875 | ||||||
|  |         BlockContainer <button> at (13,10) content-size 94.375x17 inline-block [BFC] children: not-inline | ||||||
|  |           BlockContainer <(anonymous)> at (13,10) content-size 94.375x17 flex-container(column) [FFC] children: not-inline | ||||||
|  |             BlockContainer <(anonymous)> at (13,10) content-size 94.375x17 flex-item [BFC] children: inline | ||||||
|  |               frag 0 from TextNode start: 0, length: 14, rect: [13,10 94.375x17] baseline: 13.296875 | ||||||
|  |                   "Select file..." | ||||||
|  |               TextNode <#text> | ||||||
|  |         Label <label> at (116,8) content-size 128.28125x17 inline-block [BFC] children: inline | ||||||
|  |           frag 0 from TextNode start: 0, length: 17, rect: [116,8 128.28125x17] baseline: 13.296875 | ||||||
|  |               "No file selected." | ||||||
|  |           TextNode <#text> | ||||||
|  |       TextNode <#text> | ||||||
|  |       BlockContainer <input> at (253,8) content-size 255.34375x21 inline-block [BFC] children: inline | ||||||
|  |         frag 0 from BlockContainer start: 0, length: 0, rect: [258,10 103.71875x17] baseline: 13.296875 | ||||||
|  |         frag 1 from Label start: 0, length: 0, rect: [371,8 137.625x17] baseline: 13.296875 | ||||||
|  |         BlockContainer <button> at (258,10) content-size 103.71875x17 inline-block [BFC] children: not-inline | ||||||
|  |           BlockContainer <(anonymous)> at (258,10) content-size 103.71875x17 flex-container(column) [FFC] children: not-inline | ||||||
|  |             BlockContainer <(anonymous)> at (258,10) content-size 103.71875x17 flex-item [BFC] children: inline | ||||||
|  |               frag 0 from TextNode start: 0, length: 15, rect: [258,10 103.71875x17] baseline: 13.296875 | ||||||
|  |                   "Select files..." | ||||||
|  |               TextNode <#text> | ||||||
|  |         Label <label> at (371,8) content-size 137.625x17 inline-block [BFC] children: inline | ||||||
|  |           frag 0 from TextNode start: 0, length: 18, rect: [371,8 137.625x17] baseline: 13.296875 | ||||||
|  |               "No files selected." | ||||||
|  |           TextNode <#text> | ||||||
|  |       TextNode <#text> | ||||||
|  | 
 | ||||||
|  | ViewportPaintable (Viewport<#document>) [0,0 800x600] | ||||||
|  |   PaintableWithLines (BlockContainer<HTML>) [0,0 800x600] | ||||||
|  |     PaintableWithLines (BlockContainer<BODY>) [8,8 784x21] | ||||||
|  |       PaintableWithLines (BlockContainer<INPUT>) [8,8 236.65625x21] | ||||||
|  |         PaintableWithLines (BlockContainer<BUTTON>) [8,8 104.375x21] | ||||||
|  |           PaintableWithLines (BlockContainer(anonymous)) [13,10 94.375x17] | ||||||
|  |             PaintableWithLines (BlockContainer(anonymous)) [13,10 94.375x17] | ||||||
|  |               TextPaintable (TextNode<#text>) | ||||||
|  |         PaintableWithLines (Label<LABEL>) [112,8 132.28125x17] | ||||||
|  |           TextPaintable (TextNode<#text>) | ||||||
|  |       TextPaintable (TextNode<#text>) | ||||||
|  |       PaintableWithLines (BlockContainer<INPUT>) [253,8 255.34375x21] overflow: [253,8 255.625x21] | ||||||
|  |         PaintableWithLines (BlockContainer<BUTTON>) [253,8 113.71875x21] | ||||||
|  |           PaintableWithLines (BlockContainer(anonymous)) [258,10 103.71875x17] | ||||||
|  |             PaintableWithLines (BlockContainer(anonymous)) [258,10 103.71875x17] | ||||||
|  |               TextPaintable (TextNode<#text>) | ||||||
|  |         PaintableWithLines (Label<LABEL>) [367,8 141.625x17] | ||||||
|  |           TextPaintable (TextNode<#text>) | ||||||
							
								
								
									
										2
									
								
								Tests/LibWeb/Layout/input/input-file.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Tests/LibWeb/Layout/input/input-file.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | <input type="file" /> | ||||||
|  | <input type="file" multiple /> | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| pass       text: "pass" | pass    Select file...No file selected.   text: "pass" | ||||||
| hidden: "pass" | hidden: "pass" | ||||||
| button: "pass" | button: "pass" | ||||||
| checkbox: "pass" | checkbox: "pass" | ||||||
|  |  | ||||||
|  | @ -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=image], input[type=reset], input[type=color], input[type=checkbox], input[type=radio], input[type=range]), textarea { | input:not([type=submit], input[type=button], input[type=image], input[type=reset], input[type=color], input[type=checkbox], input[type=file], 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); | ||||||
|  |  | ||||||
|  | @ -69,6 +69,8 @@ void HTMLInputElement::visit_edges(Cell::Visitor& visitor) | ||||||
|     visitor.visit(m_placeholder_element); |     visitor.visit(m_placeholder_element); | ||||||
|     visitor.visit(m_placeholder_text_node); |     visitor.visit(m_placeholder_text_node); | ||||||
|     visitor.visit(m_color_well_element); |     visitor.visit(m_color_well_element); | ||||||
|  |     visitor.visit(m_file_button); | ||||||
|  |     visitor.visit(m_file_label); | ||||||
|     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); |     visitor.visit(m_slider_thumb); | ||||||
|  | @ -80,7 +82,7 @@ JS::GCPtr<Layout::Node> HTMLInputElement::create_layout_node(NonnullRefPtr<CSS:: | ||||||
|     if (type_state() == TypeAttributeState::Hidden) |     if (type_state() == TypeAttributeState::Hidden) | ||||||
|         return nullptr; |         return nullptr; | ||||||
| 
 | 
 | ||||||
|     if (type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::Button || type_state() == TypeAttributeState::ResetButton || type_state() == TypeAttributeState::FileUpload) |     if (type_state() == TypeAttributeState::SubmitButton || type_state() == TypeAttributeState::Button || type_state() == TypeAttributeState::ResetButton) | ||||||
|         return heap().allocate_without_realm<Layout::ButtonBox>(document(), *this, move(style)); |         return heap().allocate_without_realm<Layout::ButtonBox>(document(), *this, move(style)); | ||||||
| 
 | 
 | ||||||
|     if (type_state() == TypeAttributeState::ImageButton) |     if (type_state() == TypeAttributeState::ImageButton) | ||||||
|  | @ -600,6 +602,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::FileUpload: | ||||||
|  |         create_file_input_shadow_tree(); | ||||||
|  |         break; | ||||||
|     case TypeAttributeState::Range: |     case TypeAttributeState::Range: | ||||||
|         create_range_input_shadow_tree(); |         create_range_input_shadow_tree(); | ||||||
|         break; |         break; | ||||||
|  | @ -616,6 +621,9 @@ void HTMLInputElement::update_shadow_tree() | ||||||
|     case TypeAttributeState::Color: |     case TypeAttributeState::Color: | ||||||
|         update_color_well_element(); |         update_color_well_element(); | ||||||
|         break; |         break; | ||||||
|  |     case TypeAttributeState::FileUpload: | ||||||
|  |         update_file_input_shadow_tree(); | ||||||
|  |         break; | ||||||
|     case TypeAttributeState::Range: |     case TypeAttributeState::Range: | ||||||
|         update_slider_thumb_element(); |         update_slider_thumb_element(); | ||||||
|         break; |         break; | ||||||
|  | @ -752,6 +760,53 @@ void HTMLInputElement::update_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)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void HTMLInputElement::create_file_input_shadow_tree() | ||||||
|  | { | ||||||
|  |     auto& realm = this->realm(); | ||||||
|  | 
 | ||||||
|  |     auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm, document(), *this, Bindings::ShadowRootMode::Closed); | ||||||
|  | 
 | ||||||
|  |     m_file_button = DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); | ||||||
|  |     m_file_label = DOM::create_element(document(), HTML::TagNames::label, Namespace::HTML).release_value_but_fixme_should_propagate_errors(); | ||||||
|  |     MUST(m_file_label->set_attribute(HTML::AttributeNames::style, "padding-left: 4px;"_string)); | ||||||
|  | 
 | ||||||
|  |     auto on_button_click = [this](JS::VM&) { | ||||||
|  |         show_the_picker_if_applicable(*this); | ||||||
|  |         return JS::js_undefined(); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     auto on_button_click_function = JS::NativeFunction::create(realm, move(on_button_click), 0, "", &realm); | ||||||
|  |     auto on_button_click_callback = realm.heap().allocate_without_realm<WebIDL::CallbackType>(on_button_click_function, Bindings::host_defined_environment_settings_object(realm)); | ||||||
|  |     m_file_button->add_event_listener_without_options(UIEvents::EventNames::click, DOM::IDLEventListener::create(realm, on_button_click_callback)); | ||||||
|  | 
 | ||||||
|  |     update_file_input_shadow_tree(); | ||||||
|  | 
 | ||||||
|  |     MUST(shadow_root->append_child(*m_file_button)); | ||||||
|  |     MUST(shadow_root->append_child(*m_file_label)); | ||||||
|  | 
 | ||||||
|  |     set_shadow_root(shadow_root); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void HTMLInputElement::update_file_input_shadow_tree() | ||||||
|  | { | ||||||
|  |     if (!m_file_button || !m_file_label) | ||||||
|  |         return; | ||||||
|  | 
 | ||||||
|  |     auto files_label = has_attribute(HTML::AttributeNames::multiple) ? "files"sv : "file"sv; | ||||||
|  |     m_file_button->set_text_content(MUST(String::formatted("Select {}...", files_label))); | ||||||
|  | 
 | ||||||
|  |     if (m_selected_files && m_selected_files->length() > 0) { | ||||||
|  |         if (m_selected_files->length() == 1) | ||||||
|  |             m_file_label->set_text_content(m_selected_files->item(0)->name()); | ||||||
|  |         else | ||||||
|  |             m_file_label->set_text_content(MUST(String::formatted("{} files selected.", m_selected_files->length()))); | ||||||
|  |     } else { | ||||||
|  |         m_file_label->set_text_content(MUST(String::formatted("No {} selected.", files_label))); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     document().invalidate_layout(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void HTMLInputElement::create_range_input_shadow_tree() | void HTMLInputElement::create_range_input_shadow_tree() | ||||||
| { | { | ||||||
|     auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed); |     auto shadow_root = heap().allocate<DOM::ShadowRoot>(realm(), document(), *this, Bindings::ShadowRootMode::Closed); | ||||||
|  |  | ||||||
|  | @ -226,6 +226,7 @@ private: | ||||||
|     void update_shadow_tree(); |     void update_shadow_tree(); | ||||||
|     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_file_input_shadow_tree(); | ||||||
|     void create_range_input_shadow_tree(); |     void create_range_input_shadow_tree(); | ||||||
|     WebIDL::ExceptionOr<void> run_input_activation_behavior(DOM::Event const&); |     WebIDL::ExceptionOr<void> run_input_activation_behavior(DOM::Event const&); | ||||||
|     void set_checked_within_group(); |     void set_checked_within_group(); | ||||||
|  | @ -255,6 +256,10 @@ private: | ||||||
|     void update_color_well_element(); |     void update_color_well_element(); | ||||||
|     JS::GCPtr<DOM::Element> m_color_well_element; |     JS::GCPtr<DOM::Element> m_color_well_element; | ||||||
| 
 | 
 | ||||||
|  |     void update_file_input_shadow_tree(); | ||||||
|  |     JS::GCPtr<DOM::Element> m_file_button; | ||||||
|  |     JS::GCPtr<DOM::Element> m_file_label; | ||||||
|  | 
 | ||||||
|     void update_slider_thumb_element(); |     void update_slider_thumb_element(); | ||||||
|     JS::GCPtr<DOM::Element> m_slider_thumb; |     JS::GCPtr<DOM::Element> m_slider_thumb; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Flynn
						Timothy Flynn