| // Copyright 2014 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/dom/html_element.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/lazy_instance.h" |
| #include "base/message_loop/message_loop_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "cobalt/base/console_log.h" |
| #include "cobalt/base/tokens.h" |
| #include "cobalt/cssom/absolute_url_value.h" |
| #include "cobalt/cssom/cascaded_style.h" |
| #include "cobalt/cssom/computed_style.h" |
| #include "cobalt/cssom/css_parser.h" |
| #include "cobalt/cssom/css_style_sheet.h" |
| #include "cobalt/cssom/keyword_value.h" |
| #include "cobalt/cssom/property_list_value.h" |
| #include "cobalt/cssom/selector_tree.h" |
| #include "cobalt/cssom/viewport_size.h" |
| #include "cobalt/dom/document.h" |
| #include "cobalt/dom/dom_animatable.h" |
| #include "cobalt/dom/dom_string_map.h" |
| #include "cobalt/dom/focus_event.h" |
| #include "cobalt/dom/html_anchor_element.h" |
| #include "cobalt/dom/html_audio_element.h" |
| #include "cobalt/dom/html_body_element.h" |
| #include "cobalt/dom/html_br_element.h" |
| #include "cobalt/dom/html_div_element.h" |
| #include "cobalt/dom/html_element_context.h" |
| #include "cobalt/dom/html_element_factory.h" |
| #include "cobalt/dom/html_head_element.h" |
| #include "cobalt/dom/html_heading_element.h" |
| #include "cobalt/dom/html_html_element.h" |
| #include "cobalt/dom/html_image_element.h" |
| #include "cobalt/dom/html_link_element.h" |
| #include "cobalt/dom/html_meta_element.h" |
| #include "cobalt/dom/html_paragraph_element.h" |
| #include "cobalt/dom/html_script_element.h" |
| #include "cobalt/dom/html_span_element.h" |
| #include "cobalt/dom/html_style_element.h" |
| #include "cobalt/dom/html_title_element.h" |
| #include "cobalt/dom/html_unknown_element.h" |
| #include "cobalt/dom/html_video_element.h" |
| #include "cobalt/dom/lottie_player.h" |
| #include "cobalt/dom/rule_matching.h" |
| #include "cobalt/dom/text.h" |
| #include "cobalt/loader/image/animated_image_tracker.h" |
| #include "cobalt/loader/resource_cache.h" |
| #include "cobalt/math/clamp.h" |
| #include "cobalt/web/csp_delegate.h" |
| #include "third_party/icu/source/common/unicode/uchar.h" |
| #include "third_party/icu/source/common/unicode/utf8.h" |
| |
| using cobalt::cssom::ViewportSize; |
| |
| namespace cobalt { |
| namespace dom { |
| |
| namespace { |
| |
| // If an element has a "tabindex" attribute less than -1, it will be considered |
| // a UI navigation focus item. The web spec states that all negative tabindex |
| // values are treated the same. However, since some apps may want to treat |
| // certain elements as HTML focusable but not UI navigation focus items, the |
| // commonly used value of -1 is reserved. |
| // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex |
| const int32 kUiNavFocusTabIndexThreshold = -2; |
| |
| // This custom data attribute can be used to force UI navigation to remain |
| // focused on the element for a specified duration. |
| const char kUiNavFocusDurationAttribute[] = "data-cobalt-ui-nav-focus-duration"; |
| |
| // https://www.w3.org/TR/resource-timing-1/#dom-performanceresourcetiming-initiatortype |
| const char* kPerformanceResourceTimingInitiatorType = "img"; |
| |
| void UiNavCallbackHelper( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner, |
| base::Callback<void(SbTimeMonotonic)> callback) { |
| task_runner->PostTask(FROM_HERE, |
| base::Bind(callback, SbTimeGetMonotonicNow())); |
| } |
| |
| struct NonTrivialStaticFields { |
| NonTrivialStaticFields() { |
| cssom::PropertyKeyVector computed_style_invalidation_properties; |
| cssom::PropertyKeyVector layout_box_invalidation_properties; |
| cssom::PropertyKeyVector size_invalidation_properties; |
| cssom::PropertyKeyVector cross_references_invalidation_properties; |
| |
| for (int i = 0; i <= cssom::kMaxLonghandPropertyKey; ++i) { |
| cssom::PropertyKey property_key = static_cast<cssom::PropertyKey>(i); |
| |
| if (cssom::GetPropertyImpactsChildComputedStyle(property_key) == |
| cssom::kImpactsChildComputedStyleYes) { |
| computed_style_invalidation_properties.push_back(property_key); |
| } |
| |
| // TODO: Revisit inherited property handling. Currently, all boxes are |
| // invalidated if an inherited property changes, but now that inherited |
| // properties dynamically update, this is likely no longer necessary. |
| if (cssom::GetPropertyInheritance(property_key) == cssom::kInheritedYes || |
| cssom::GetPropertyImpactsBoxGeneration(property_key) == |
| cssom::kImpactsBoxGenerationYes) { |
| layout_box_invalidation_properties.push_back(property_key); |
| } else { |
| if (cssom::GetPropertyImpactsBoxSizes(property_key) == |
| cssom::kImpactsBoxSizesYes) { |
| size_invalidation_properties.push_back(property_key); |
| } |
| if (cssom::GetPropertyImpactsBoxCrossReferences(property_key) == |
| cssom::kImpactsBoxCrossReferencesYes) { |
| cross_references_invalidation_properties.push_back(property_key); |
| } |
| } |
| } |
| |
| computed_style_invalidation_property_checker = |
| cssom::CSSComputedStyleData::PropertySetMatcher( |
| computed_style_invalidation_properties); |
| layout_box_invalidation_property_checker = |
| cssom::CSSComputedStyleData::PropertySetMatcher( |
| layout_box_invalidation_properties); |
| size_invalidation_property_checker = |
| cssom::CSSComputedStyleData::PropertySetMatcher( |
| size_invalidation_properties); |
| cross_references_invalidation_property_checker = |
| cssom::CSSComputedStyleData::PropertySetMatcher( |
| cross_references_invalidation_properties); |
| } |
| |
| cssom::CSSComputedStyleData::PropertySetMatcher |
| computed_style_invalidation_property_checker; |
| cssom::CSSComputedStyleData::PropertySetMatcher |
| layout_box_invalidation_property_checker; |
| cssom::CSSComputedStyleData::PropertySetMatcher |
| size_invalidation_property_checker; |
| cssom::CSSComputedStyleData::PropertySetMatcher |
| cross_references_invalidation_property_checker; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(NonTrivialStaticFields); |
| }; |
| |
| base::LazyInstance<NonTrivialStaticFields>::DestructorAtExit |
| non_trivial_static_fields = LAZY_INSTANCE_INITIALIZER; |
| |
| } // namespace |
| |
| void HTMLElement::RuleMatchingState::Clear() { |
| if (is_set) { |
| is_set = false; |
| |
| parent_matching_nodes.clear(); |
| parent_descendant_nodes.clear(); |
| |
| previous_sibling_matching_nodes.clear(); |
| previous_sibling_following_sibling_nodes.clear(); |
| |
| matching_nodes_parent_nodes.clear(); |
| matching_nodes.clear(); |
| |
| are_descendant_nodes_dirty = true; |
| descendant_nodes.clear(); |
| |
| are_following_sibling_nodes_dirty = true; |
| following_sibling_nodes.clear(); |
| } |
| } |
| |
| std::string HTMLElement::dir() const { |
| // The dir attribute is limited to only known values. On getting, dir must |
| // return the conforming value associated with the state the attribute is in, |
| // or the empty string if the attribute is in a state with no associated |
| // keyword value. |
| // https://html.spec.whatwg.org/commit-snapshots/ebcac971c2add28a911283899da84ec509876c44/#the-dir-attribute |
| if (dir_ == kDirAuto) { |
| return "auto"; |
| } else if (dir_ == kDirLeftToRight) { |
| return "ltr"; |
| } else if (dir_ == kDirRightToLeft) { |
| return "rtl"; |
| } else { |
| return ""; |
| } |
| } |
| |
| void HTMLElement::set_dir(const std::string& value) { |
| // Funnel through OnSetAttribute. |
| SetAttribute("dir", value); |
| } |
| |
| scoped_refptr<DOMStringMap> HTMLElement::dataset() { |
| scoped_refptr<DOMStringMap> dataset(dataset_); |
| if (!dataset) { |
| // Create a new instance and store a weak reference. |
| dataset = new DOMStringMap(this); |
| dataset_ = dataset->AsWeakPtr(); |
| } |
| return dataset; |
| } |
| |
| int32 HTMLElement::tab_index() const { |
| if (tabindex_) { |
| return *tabindex_; |
| } |
| LOG(WARNING) << "Element's tabindex is not valid."; |
| // The default value is 0 for focusable elements. |
| // https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex |
| return 0; |
| } |
| |
| void HTMLElement::set_tab_index(int32 tab_index) { |
| SetAttribute("tabindex", base::Int32ToString(tab_index)); |
| } |
| |
| // Algorithm for Focus: |
| // https://www.w3.org/TR/html50/editing.html#dom-focus |
| void HTMLElement::Focus() { |
| // 1. If the element is marked as locked for focus, then abort these steps. |
| if (locked_for_focus_) { |
| return; |
| } |
| |
| // 2. Mark the element as locked for focus. |
| locked_for_focus_ = true; |
| |
| // 3. Run the focusing steps for the element. |
| RunFocusingSteps(); |
| |
| // 4. Unmark the element as locked for focus. |
| locked_for_focus_ = false; |
| |
| // Custom, not in any spec. |
| Document* document = node_document(); |
| if (document) { |
| document->OnFocusChange(); |
| } |
| } |
| |
| // Algorithm for Blur: |
| // https://www.w3.org/TR/html50/editing.html#dom-blur |
| void HTMLElement::Blur() { |
| // The blur() method, when invoked, should run the unfocusing steps for the |
| // element on which the method was called instead. User agents may selectively |
| // or uniformly ignore calls to this method for usability reasons. |
| RunUnFocusingSteps(); |
| |
| // Custom, not in any spec. |
| Document* document = node_document(); |
| if (document) { |
| document->OnFocusChange(); |
| } |
| } |
| |
| // Algorithm for GetClientRects: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-getclientrects |
| scoped_refptr<DOMRectList> HTMLElement::GetClientRects() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element on which it was invoked does not have an associated |
| // layout box return an empty DOMRectList object and stop this algorithm. |
| if (!layout_boxes_) { |
| return base::WrapRefCounted(new DOMRectList()); |
| } |
| |
| // The remaining steps are implemented in LayoutBoxes::GetClientRects(). |
| return layout_boxes_->GetClientRects(); |
| } |
| |
| // Algorithm for client_top: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-clienttop |
| float HTMLElement::client_top() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element has no associated CSS layout box or if the CSS layout box |
| // is inline, return zero. |
| if (!layout_boxes_ || layout_boxes_->IsInline()) { |
| return 0.0f; |
| } |
| // 2. Return the computed value of the 'border-top-width' property plus the |
| // height of any scrollbar rendered between the top padding edge and the top |
| // border edge, ignoring any transforms that apply to the element and its |
| // ancestors. |
| return layout_boxes_->GetBorderTopWidth(); |
| } |
| |
| // Algorithm for client_left: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-clientleft |
| float HTMLElement::client_left() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element has no associated CSS layout box or if the CSS layout box |
| // is inline, return zero. |
| if (!layout_boxes_ || layout_boxes_->IsInline()) { |
| return 0.0f; |
| } |
| // 2. Return the computed value of the 'border-left-width' property plus the |
| // width of any scrollbar rendered between the left padding edge and the left |
| // border edge, ignoring any transforms that apply to the element and its |
| // ancestors. |
| return layout_boxes_->GetBorderLeftWidth(); |
| } |
| |
| // Algorithm for client_width: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-clientwidth |
| float HTMLElement::client_width() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element has no associated CSS layout box or if the CSS layout box |
| // is inline, return zero. |
| if (!layout_boxes_ || layout_boxes_->IsInline()) { |
| return 0.0f; |
| } |
| |
| // 2. If the element is the root element, return the viewport width. |
| if (IsRootElement()) { |
| return layout_boxes_->GetMarginEdgeWidth(); |
| } |
| |
| // 3. Return the width of the padding edge, ignoring any transforms that apply |
| // to the element and its ancestors. |
| return layout_boxes_->GetPaddingEdgeWidth(); |
| } |
| |
| // Algorithm for client_height: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-clientheight |
| float HTMLElement::client_height() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element has no associated CSS layout box or if the CSS layout box |
| // is inline, return zero. |
| if (!layout_boxes_ || layout_boxes_->IsInline()) { |
| return 0.0f; |
| } |
| |
| // 2. If the element is the root element, return the viewport height. |
| if (IsRootElement()) { |
| return layout_boxes_->GetMarginEdgeHeight(); |
| } |
| |
| // Return the height of the padding edge, ignoring any transforms that apply |
| // to the element and its ancestors. |
| return layout_boxes_->GetPaddingEdgeHeight(); |
| } |
| |
| // Algorithm for scrollWidth: |
| // https://www.w3.org/TR/cssom-view-1/#dom-element-scrollwidth |
| int32 HTMLElement::scroll_width() { |
| // 1. Let document be the element's node document. |
| // 2. If document is not the active document, return zero and terminate |
| // these steps. |
| if (!node_document() || !node_document()->window()) { |
| return 0; |
| } |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| int32 element_scroll_width = 0; |
| if (layout_boxes_) { |
| element_scroll_width = static_cast<int32>( |
| layout_boxes_->GetScrollArea(directionality()).width()); |
| } |
| |
| // 3. Let viewport width be the width of the viewport excluding the width of |
| // the scroll bar, if any, or zero if there is no viewport. |
| // 4. If the element is the root element and document is not in quirks mode |
| // return max(viewport scrolling area width, viewport width). |
| // 5. If the element is the HTML body element, document is in quirks mode |
| // and the element is not potentially scrollable, return max(viewport |
| // scrolling area width, viewport width). |
| if (IsRootElement()) { |
| return std::max(node_document()->viewport_size().width(), |
| element_scroll_width); |
| } |
| |
| // 6. If the element does not have any associated CSS layout box return zero |
| // and terminate these steps. |
| // 7. Return the width of the element's scrolling area. |
| return element_scroll_width; |
| } |
| |
| // Algorithm for scrollHeight: |
| // https://www.w3.org/TR/cssom-view-1/#dom-element-scrollheight |
| int32 HTMLElement::scroll_height() { |
| // 1. Let document be the element's node document. |
| // 2. If document is not the active document, return zero and terminate |
| // these steps. |
| if (!node_document() || !node_document()->window()) { |
| return 0; |
| } |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| int32 element_scroll_height = 0; |
| if (layout_boxes_) { |
| element_scroll_height = static_cast<int32>( |
| layout_boxes_->GetScrollArea(directionality()).height()); |
| } |
| |
| // 3. Let viewport height be the height of the viewport excluding the height |
| // of the scroll bar, if any, or zero if there is no viewport. |
| // 4. If the element is the root element and document is not in quirks mode |
| // return max(viewport scrolling area height, viewport height). |
| // 5. If the element is the HTML body element, document is in quirks mode |
| // and the element is not potentially scrollable, return max(viewport |
| // scrolling area height, viewport height). |
| if (IsRootElement()) { |
| return std::max(node_document()->viewport_size().height(), |
| element_scroll_height); |
| } |
| |
| // 6. If the element does not have any associated CSS layout box return zero |
| // and terminate these steps. |
| // 7. Return the height of the element's scrolling area. |
| return element_scroll_height; |
| } |
| |
| // Algorithm for scrollLeft: |
| // https://www.w3.org/TR/cssom-view-1/#dom-element-scrollleft |
| // For RTL, the rightmost content is visible when scrollLeft == 0, and |
| // scrollLeft values are <= 0. Chrome does not behave this way currently, but |
| // it is the spec that has been adopted. |
| // https://readable-email.org/list/www-style/topic/cssom-view-value-of-scrollleft-in-rtl-situations-is-completely-busted-across-browsers |
| float HTMLElement::scroll_left() { |
| // This is only partially implemented and will only work for elements with |
| // UI navigation containers. |
| |
| // 1. Let document be the element's node document. |
| // 2. If document is not the active document, return zero and terminate these |
| // steps. |
| // 3. Let window be the value of document's defaultView attribute. |
| // 4. If window is null, return zero and terminate these steps. |
| if (!node_document() || !node_document()->window()) { |
| return 0.0f; |
| } |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| if (!ui_nav_item_ || !ui_nav_item_->IsContainer()) { |
| return 0.0f; |
| } |
| |
| // 5. If the element is the root element and document is in quirks mode, |
| // return zero and terminate these steps. |
| // 6. If the element is the root element return the value of scrollX on |
| // window. |
| // 7. If the element is the HTML body element, document is in quirks mode, and |
| // the element is not potentially scrollable, return the value of scrollX |
| // on window. |
| |
| // 8. If the element does not have any associated CSS layout box, return zero |
| // and terminate these steps. |
| if (!layout_boxes_) { |
| return 0.0f; |
| } |
| |
| // 9. Return the x-coordinate of the scrolling area at the alignment point |
| // with the left of the padding edge of the element. |
| float left, top; |
| ui_nav_item_->GetContentOffset(&left, &top); |
| return left; |
| } |
| |
| // Algorithm for scrollTop: |
| // https://www.w3.org/TR/cssom-view-1/#dom-element-scrolltop |
| float HTMLElement::scroll_top() { |
| // This is only partially implemented and will only work for elements with |
| // UI navigation containers. |
| |
| // 1. Let document be the element's node document. |
| // 2. If document is not the active document, return zero and terminate these |
| // steps. |
| // 3. Let window be the value of document's defaultView attribute. |
| // 4. If window is null, return zero and terminate these steps. |
| if (!node_document() || !node_document()->window()) { |
| return 0.0f; |
| } |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| if (!ui_nav_item_ || !ui_nav_item_->IsContainer()) { |
| return 0.0f; |
| } |
| |
| // 5. If the element is the root element and document is in quirks mode, |
| // return zero and terminate these steps. |
| // 6. If the element is the root element return the value of scrollY on |
| // window. |
| // 7. If the element is the HTML body element, document is in quirks mode, and |
| // the element is not potentially scrollable, return the value of scrollY |
| // on window. |
| |
| // 8. If the element does not have any associated CSS layout box, return zero |
| // and terminate these steps. |
| if (!layout_boxes_) { |
| return 0.0f; |
| } |
| |
| // 9. Return the y-coordinate of the scrolling area at the alignment point |
| // with the top of the padding edge of the element. |
| float left, top; |
| ui_nav_item_->GetContentOffset(&left, &top); |
| return top; |
| } |
| |
| // Algorithm for scrollLeft: |
| // https://www.w3.org/TR/cssom-view-1/#dom-element-scrollleft |
| void HTMLElement::set_scroll_left(float x) { |
| // This is only partially implemented and will only work for elements with |
| // UI navigation containers. |
| |
| // 1. Let x be the given value. |
| // 2. Normalize non-finite values for x. |
| |
| // 3. Let document be the element's node document. |
| // 4. If document is not the active document, terminate these steps. |
| // 5. Let window be the value of document's defaultView attribute. |
| // 6. If window is null, terminate these steps. |
| if (!node_document() || !node_document()->window()) { |
| return; |
| } |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| if (!ui_nav_item_ || !ui_nav_item_->IsContainer()) { |
| CLOG(WARNING, debugger_hooks()) |
| << "scrollLeft only works on HTML elements with 'overflow' set to " |
| << "'scroll' or 'auto'"; |
| return; |
| } |
| |
| // 7. If the element is the root element and document is in quirks mode, |
| // terminate these steps. |
| // 8. If the element is the root element invoke scroll() on window with x as |
| // first argument and scrollY on window as second argument, and terminate |
| // these steps. |
| // 9. If the element is the HTML body element, document is in quirks mode, and |
| // the element is not potentially scrollable, invoke scroll() on window |
| // with x as first argument and scrollY on window as second argument, and |
| // terminate these steps. |
| |
| // 10. If the element does not have any associated CSS layout box, the element |
| // has no associated scrolling box, or the element has no overflow, |
| // terminate these steps. |
| if (!layout_boxes_ || |
| scroll_width() <= layout_boxes_->GetPaddingEdgeWidth()) { |
| // Make sure the UI navigation container is set to the expected 0. |
| x = 0.0f; |
| } |
| |
| // 11. Scroll the element to x,scrollTop, with the scroll behavior being |
| // "auto". |
| node_document()->window()->CancelScroll(ui_nav_item_); |
| float left, top; |
| ui_nav_item_->GetContentOffset(&left, &top); |
| |
| float throwaway, min = x, max = x; |
| ui_nav_item_->GetBounds(&throwaway, &min, &throwaway, &max); |
| x = math::Clamp(x, min, max); |
| ui_nav_item_->SetContentOffset(x, top); |
| } |
| |
| // Algorithm for scrollTop: |
| // https://www.w3.org/TR/cssom-view-1/#dom-element-scrolltop |
| void HTMLElement::set_scroll_top(float y) { |
| // This is only partially implemented and will only work for elements with |
| // UI navigation containers. |
| |
| // 1. Let y be the given value. |
| // 2. Normalize non-finite values for y. |
| |
| // 3. Let document be the element's node document. |
| // 4. If document is not the active document, terminate these steps. |
| // 5. Let window be the value of document's defaultView attribute. |
| // 6. If window is null, terminate these steps. |
| if (!node_document() || !node_document()->window()) { |
| return; |
| } |
| |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| if (!ui_nav_item_ || !ui_nav_item_->IsContainer()) { |
| CLOG(WARNING, debugger_hooks()) |
| << "scrollTop only works on HTML elements with 'overflow' set to " |
| << "'scroll' or 'auto'"; |
| return; |
| } |
| |
| // 7. If the element is the root element and document is in quirks mode, |
| // terminate these steps. |
| // 8. If the element is the root element invoke scroll() on window with |
| // scrollX on window as first argument and y as second argument, and |
| // terminate these steps. |
| // 9. If the element is the HTML body element, document is in quirks mode, and |
| // the element is not potentially scrollable, invoke scroll() on window |
| // with scrollX as first argument and y as second argument, and terminate |
| // these steps. |
| |
| // 10. If the element does not have any associated CSS layout box, the element |
| // has no associated scrolling box, or the element has no overflow, |
| // terminate these steps. |
| if (!layout_boxes_ || |
| scroll_height() <= layout_boxes_->GetPaddingEdgeHeight()) { |
| // Make sure the UI navigation container is set to the expected 0. |
| y = 0.0f; |
| } |
| |
| // 11. Scroll the element to scrollLeft,y, with the scroll behavior being |
| // "auto". |
| node_document()->window()->CancelScroll(ui_nav_item_); |
| float left, top; |
| ui_nav_item_->GetContentOffset(&left, &top); |
| |
| float throwaway, min = y, max = y; |
| ui_nav_item_->GetBounds(&min, &throwaway, &max, &throwaway); |
| y = math::Clamp(y, min, max); |
| ui_nav_item_->SetContentOffset(left, y); |
| } |
| |
| // Algorithm for offsetParent: |
| // https://drafts.csswg.org/date/2019-10-11/cssom-view/#extensions-to-the-htmlelement-interface |
| Element* HTMLElement::offset_parent() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If any of the following holds true return null and terminate this |
| // algorithm: |
| // . The element does not have an associated CSS layout box. |
| // . The element is the root element. |
| // . The element is the HTML body element. |
| // . The element's computed value of the 'position' property is 'fixed'. |
| if (!layout_boxes_ || IsRootElement() || AsHTMLBodyElement() || |
| !computed_style() || |
| computed_style()->position() == cssom::KeywordValue::GetFixed()) { |
| return NULL; |
| } |
| |
| // 2. Return the nearest ancestor element of the element for which at least |
| // one of the following is true and terminate this algorithm if such an |
| // ancestor is found: |
| // . The element is a containing block of absolutely-positioned descendants |
| // (regardless of whether there are any absolutely-positioned |
| // descendants). |
| // . It is the HTML body element. |
| for (Node* ancestor_node = parent_node(); ancestor_node; |
| ancestor_node = ancestor_node->parent_node()) { |
| Element* ancestor_element = ancestor_node->AsElement(); |
| if (!ancestor_element) { |
| continue; |
| } |
| HTMLElement* ancestor_html_element = ancestor_element->AsHTMLElement(); |
| if (!ancestor_html_element) { |
| continue; |
| } |
| DCHECK(ancestor_html_element->computed_style()); |
| if (ancestor_html_element->AsHTMLBodyElement() || |
| ancestor_html_element->computed_style() |
| ->IsContainingBlockForPositionAbsoluteElements()) { |
| return ancestor_element; |
| } |
| } |
| |
| // 3. Return null. |
| return NULL; |
| } |
| |
| // Algorithm for offset_top: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-htmlelement-offsettop |
| float HTMLElement::offset_top() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element is the HTML body element or does not have any associated |
| // CSS layout box return zero and terminate this algorithm. |
| if (!layout_boxes_ || AsHTMLBodyElement()) { |
| return 0.0f; |
| } |
| |
| // 2. If the offsetParent of the element is null return the y-coordinate of |
| // the top border edge of the first CSS layout box associated with the |
| // element, relative to the initial containing block origin, ignoring any |
| // transforms that apply to the element and its ancestors, and terminate this |
| // algorithm. |
| scoped_refptr<Element> offset_parent_element = offset_parent(); |
| if (!offset_parent_element) { |
| return layout_boxes_->GetBorderEdgeTop(); |
| } |
| |
| // 3. Return the result of subtracting the y-coordinate of the top padding |
| // edge of the first CSS layout box associated with the offsetParent of the |
| // element from the y-coordinate of the top border edge of the first CSS |
| // layout box associated with the element, relative to the initial containing |
| // block origin, ignoring any transforms that apply to the element and its |
| // ancestors. |
| scoped_refptr<HTMLElement> offset_parent_html_element = |
| offset_parent_element->AsHTMLElement(); |
| if (!offset_parent_html_element) { |
| return layout_boxes_->GetBorderEdgeTop(); |
| } |
| |
| DCHECK(offset_parent_html_element->layout_boxes()); |
| return layout_boxes_->GetBorderEdgeTop() - |
| offset_parent_html_element->layout_boxes()->GetPaddingEdgeOffset().y(); |
| } |
| |
| // Algorithm for offset_left: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-htmlelement-offsetleft |
| float HTMLElement::offset_left() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element is the HTML body element or does not have any associated |
| // CSS layout box return zero and terminate this algorithm. |
| if (!layout_boxes_ || AsHTMLBodyElement()) { |
| return 0.0f; |
| } |
| |
| // 2. If the offsetParent of the element is null return the x-coordinate of |
| // the left border edge of the first CSS layout box associated with the |
| // element, relative to the initial containing block origin, ignoring any |
| // transforms that apply to the element and its ancestors, and terminate this |
| // algorithm. |
| scoped_refptr<Element> offset_parent_element = offset_parent(); |
| if (!offset_parent_element) { |
| return layout_boxes_->GetBorderEdgeLeft(); |
| } |
| |
| // 3. Return the result of subtracting the x-coordinate of the left padding |
| // edge of the first CSS layout box associated with the offsetParent of the |
| // element from the x-coordinate of the left border edge of the first CSS |
| // layout box associated with the element, relative to the initial containing |
| // block origin, ignoring any transforms that apply to the element and its |
| // ancestors. |
| scoped_refptr<HTMLElement> offset_parent_html_element = |
| offset_parent_element->AsHTMLElement(); |
| if (!offset_parent_html_element) { |
| return layout_boxes_->GetBorderEdgeLeft(); |
| } |
| |
| DCHECK(offset_parent_html_element->layout_boxes()); |
| return layout_boxes_->GetBorderEdgeLeft() - |
| offset_parent_html_element->layout_boxes()->GetPaddingEdgeOffset().x(); |
| } |
| |
| // Algorithm for offset_width: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-htmlelement-offsetwidth |
| float HTMLElement::offset_width() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element does not have any associated CSS layout box return zero |
| // and terminate this algorithm. |
| if (!layout_boxes_) { |
| if (IsRootElement()) { |
| node_document()->OnRootElementUnableToProvideOffsetDimensions(); |
| } |
| return 0.0f; |
| } |
| |
| // 2. Return the border edge width of the first CSS layout box associated with |
| // the element, ignoring any transforms that apply to the element and its |
| // ancestors. |
| return layout_boxes_->GetBorderEdgeWidth(); |
| } |
| |
| // Algorithm for offset_height: |
| // https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-htmlelement-offsetheight |
| float HTMLElement::offset_height() { |
| DCHECK(node_document()); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| node_document()->DoSynchronousLayout(); |
| |
| // 1. If the element does not have any associated CSS layout box return zero |
| // and terminate this algorithm. |
| if (!layout_boxes_) { |
| if (IsRootElement()) { |
| node_document()->OnRootElementUnableToProvideOffsetDimensions(); |
| } |
| return 0.0f; |
| } |
| |
| // 2. Return the border edge height of the first CSS layout box associated |
| // with the element, ignoring any transforms that apply to the element and its |
| // ancestors. |
| return layout_boxes_->GetBorderEdgeHeight(); |
| } |
| |
| scoped_refptr<Node> HTMLElement::Duplicate() const { |
| Document* document = node_document(); |
| DCHECK(document->html_element_context()->html_element_factory()); |
| scoped_refptr<HTMLElement> new_html_element = |
| document->html_element_context() |
| ->html_element_factory() |
| ->CreateHTMLElement(document, local_name()); |
| new_html_element->CopyAttributes(*this); |
| new_html_element->style_->AssignFrom(*this->style_); |
| |
| // Copy cached attributes. |
| new_html_element->tabindex_ = tabindex_; |
| new_html_element->dir_ = dir_; |
| |
| return new_html_element; |
| } |
| |
| base::Optional<std::string> HTMLElement::GetStyleAttribute() const { |
| base::Optional<std::string> value = Element::GetStyleAttribute(); |
| return value.value_or(style_->css_text(NULL)); |
| } |
| |
| void HTMLElement::SetStyleAttribute(const std::string& value) { |
| Document* document = node_document(); |
| web::CspDelegate* csp_delegate = document->GetCSPDelegate(); |
| if (value.empty() || |
| csp_delegate->AllowInline( |
| web::CspDelegate::kStyle, |
| base::SourceLocation(GetSourceLocationName(), 1, 1), value)) { |
| style_->set_css_text(value, NULL); |
| Element::SetStyleAttribute(value); |
| } else { |
| // Report a violation. |
| PostToDispatchEventName(FROM_HERE, base::Tokens::error()); |
| } |
| } |
| |
| void HTMLElement::RemoveStyleAttribute() { |
| style_->set_css_text("", NULL); |
| Element::RemoveStyleAttribute(); |
| } |
| |
| void HTMLElement::OnCSSMutation() { |
| // Invalidate the computed style of this node. |
| computed_style_valid_ = false; |
| descendant_computed_styles_valid_ = false; |
| |
| // Remove the style attribute value from the Element. |
| Element::RemoveStyleAttribute(); |
| |
| node_document()->OnElementInlineStyleMutation(); |
| } |
| |
| scoped_refptr<HTMLAnchorElement> HTMLElement::AsHTMLAnchorElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLAudioElement> HTMLElement::AsHTMLAudioElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLBodyElement> HTMLElement::AsHTMLBodyElement() { return NULL; } |
| |
| scoped_refptr<HTMLBRElement> HTMLElement::AsHTMLBRElement() { return NULL; } |
| |
| scoped_refptr<HTMLDivElement> HTMLElement::AsHTMLDivElement() { return NULL; } |
| |
| scoped_refptr<HTMLHeadElement> HTMLElement::AsHTMLHeadElement() { return NULL; } |
| |
| scoped_refptr<HTMLHeadingElement> HTMLElement::AsHTMLHeadingElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLHtmlElement> HTMLElement::AsHTMLHtmlElement() { return NULL; } |
| |
| scoped_refptr<HTMLImageElement> HTMLElement::AsHTMLImageElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLLinkElement> HTMLElement::AsHTMLLinkElement() { return NULL; } |
| |
| scoped_refptr<HTMLMetaElement> HTMLElement::AsHTMLMetaElement() { return NULL; } |
| |
| scoped_refptr<HTMLMediaElement> HTMLElement::AsHTMLMediaElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLParagraphElement> HTMLElement::AsHTMLParagraphElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLScriptElement> HTMLElement::AsHTMLScriptElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLSpanElement> HTMLElement::AsHTMLSpanElement() { return NULL; } |
| |
| scoped_refptr<HTMLStyleElement> HTMLElement::AsHTMLStyleElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLTitleElement> HTMLElement::AsHTMLTitleElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLUnknownElement> HTMLElement::AsHTMLUnknownElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<HTMLVideoElement> HTMLElement::AsHTMLVideoElement() { |
| return NULL; |
| } |
| |
| scoped_refptr<LottiePlayer> HTMLElement::AsLottiePlayer() { return NULL; } |
| |
| void HTMLElement::ClearRuleMatchingState() { |
| ClearRuleMatchingStateInternal(true /*invalidate_descendants*/); |
| } |
| |
| void HTMLElement::ClearRuleMatchingStateInternal(bool invalidate_descendants) { |
| rule_matching_state_.Clear(); |
| matching_rules_valid_ = false; |
| |
| if (invalidate_descendants) { |
| InvalidateMatchingRulesRecursivelyInternal(true /*is_initial_element*/); |
| } |
| } |
| |
| void HTMLElement::ClearRuleMatchingStateOnElementAndAncestors( |
| bool invalidate_matching_rules) { |
| Element* parent_element = this->parent_element(); |
| HTMLElement* parent_html_element = |
| parent_element ? parent_element->AsHTMLElement() : NULL; |
| if (parent_html_element) { |
| parent_html_element->ClearRuleMatchingStateOnElementAndAncestors( |
| invalidate_matching_rules); |
| } |
| ClearRuleMatchingStateInternal(invalidate_matching_rules && |
| parent_html_element == NULL); |
| } |
| |
| void HTMLElement::ClearRuleMatchingStateOnElementAndDescendants() { |
| ClearRuleMatchingStateInternal(false /* invalidate_descendants*/); |
| for (Element* element = first_element_child(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->ClearRuleMatchingStateOnElementAndDescendants(); |
| } |
| } |
| } |
| |
| void HTMLElement::ClearRuleMatchingStateOnElementAndSiblingsAndDescendants() { |
| HTMLElement::ClearRuleMatchingStateOnElementAndDescendants(); |
| for (Element* element = previous_element_sibling(); element; |
| element = element->previous_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->ClearRuleMatchingStateOnElementAndDescendants(); |
| } |
| } |
| for (Element* element = next_element_sibling(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->ClearRuleMatchingStateOnElementAndDescendants(); |
| } |
| } |
| } |
| |
| void HTMLElement::InvalidateMatchingRulesRecursively() { |
| InvalidateMatchingRulesRecursivelyInternal(true /*is_initial_element*/); |
| } |
| |
| void HTMLElement::InvalidateMatchingRulesRecursivelyInternal( |
| bool is_initial_element) { |
| matching_rules_valid_ = false; |
| |
| // Invalidate matching rules on all children. |
| for (Element* element = first_element_child(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->InvalidateMatchingRulesRecursivelyInternal( |
| false /*is_initial_element*/); |
| } |
| } |
| |
| // Invalidate matching rules on all following siblings if this is the initial |
| // element and sibling combinators are used; if this is not the initial |
| // element, then these will already be handled by a previous call. |
| if (is_initial_element && |
| node_document()->selector_tree()->has_sibling_combinators()) { |
| for (Element* element = next_element_sibling(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->InvalidateMatchingRulesRecursivelyInternal( |
| false /*is_initial_element*/); |
| } |
| } |
| } |
| } |
| |
| void HTMLElement::UpdateMatchingRules() { UpdateElementMatchingRules(this); } |
| |
| void HTMLElement::UpdateMatchingRulesRecursively() { |
| UpdateMatchingRules(); |
| for (Element* element = first_element_child(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->UpdateMatchingRulesRecursively(); |
| } |
| } |
| } |
| |
| void HTMLElement::OnMatchingRulesModified() { |
| dom_stat_tracker_->OnUpdateMatchingRules(); |
| computed_style_valid_ = false; |
| } |
| |
| void HTMLElement::OnPseudoElementMatchingRulesModified() { |
| pseudo_elements_computed_styles_valid_ = false; |
| } |
| |
| void HTMLElement::UpdateComputedStyleRecursively( |
| const scoped_refptr<cssom::CSSComputedStyleDeclaration>& |
| parent_computed_style_declaration, |
| const scoped_refptr<const cssom::CSSComputedStyleData>& root_computed_style, |
| const base::TimeDelta& style_change_event_time, bool ancestors_were_valid, |
| int current_element_depth) { |
| int max_depth = node_document()->dom_max_element_depth(); |
| if (max_depth > 0 && current_element_depth >= max_depth) { |
| return; |
| } |
| |
| // Update computed styles for this element if any aren't valid. |
| bool is_valid = ancestors_were_valid && AreComputedStylesValid(); |
| if (!is_valid) { |
| UpdateComputedStyle(parent_computed_style_declaration, root_computed_style, |
| style_change_event_time, kAncestorsAreDisplayed); |
| UpdateUiNavigation(); |
| node_document()->set_ui_nav_needs_layout(true); |
| } else if (ui_nav_needs_update_) { |
| UpdateUiNavigation(); |
| } |
| |
| // Do not update computed style for descendants of "display: none" elements, |
| // since they do not participate in layout. Note the "display: none" elements |
| // themselves still need to have their computed style updated, in case the |
| // value of display is changed. |
| if (computed_style()->display() == cssom::KeywordValue::GetNone()) { |
| return; |
| } |
| |
| // Update computed style for this element's descendants. Note that if |
| // descendant_computed_styles_valid_ flag is not set, the ancestors should |
| // still be considered invalid, which forces the computes styles to be updated |
| // on all children. |
| for (Element* element = first_element_child(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| html_element->UpdateComputedStyleRecursively( |
| css_computed_style_declaration(), root_computed_style, |
| style_change_event_time, |
| is_valid && descendant_computed_styles_valid_, |
| current_element_depth + 1); |
| } |
| } |
| |
| descendant_computed_styles_valid_ = true; |
| } |
| |
| void HTMLElement::MarkNotDisplayedOnNodeAndDescendants() { |
| // While we do want to clear the animations immediately, we also want to |
| // ensure that they are also reset starting with the next computed style |
| // update. This ensures that for example a transition will not be triggered |
| // on the next computed style update. |
| ancestors_are_displayed_ = kAncestorsAreNotDisplayed; |
| |
| PurgeCachedBackgroundImages(); |
| |
| if (!css_animations_.empty() || !css_transitions_.empty()) { |
| css_transitions_.Clear(); |
| css_animations_.Clear(); |
| computed_style_valid_ = false; |
| descendant_computed_styles_valid_ = false; |
| } |
| |
| // Also clear out all animations/transitions on pseudo elements. |
| for (auto& pseudo_element : pseudo_elements_) { |
| if (pseudo_element) { |
| pseudo_element->css_transitions()->Clear(); |
| pseudo_element->css_animations()->Clear(); |
| } |
| } |
| |
| ReleaseUiNavigationItem(); |
| MarkNotDisplayedOnDescendants(); |
| } |
| |
| void HTMLElement::PurgeCachedBackgroundImagesOfNodeAndDescendants() { |
| PurgeCachedBackgroundImages(); |
| PurgeCachedBackgroundImagesOfDescendants(); |
| } |
| |
| void HTMLElement::PurgeCachedBackgroundImages() { |
| ClearActiveBackgroundImages(); |
| if (!cached_background_images_.empty()) { |
| ClearCachedBackgroundImages(); |
| computed_style_valid_ = false; |
| descendant_computed_styles_valid_ = false; |
| } |
| } |
| |
| bool HTMLElement::IsDisplayed() const { |
| return ancestors_are_displayed_ == kAncestorsAreDisplayed && |
| computed_style()->display() != cssom::KeywordValue::GetNone(); |
| } |
| |
| bool HTMLElement::IsRootElement() { |
| // The html element represents the root of an HTML document. |
| // https://www.w3.org/TR/2014/REC-html5-20141028/semantics.html#the-root-element |
| return AsHTMLHtmlElement().get() != NULL; |
| } |
| |
| void HTMLElement::InvalidateComputedStylesOfNodeAndDescendants() { |
| computed_style_valid_ = false; |
| descendant_computed_styles_valid_ = false; |
| InvalidateComputedStylesOfDescendants(); |
| } |
| |
| void HTMLElement::InvalidateLayoutBoxesOfNodeAndAncestors() { |
| InvalidateLayoutBoxes(); |
| InvalidateLayoutBoxesOfAncestors(); |
| } |
| |
| void HTMLElement::InvalidateLayoutBoxesOfNodeAndDescendants() { |
| InvalidateLayoutBoxes(); |
| InvalidateLayoutBoxesOfDescendants(); |
| } |
| |
| void HTMLElement::InvalidateLayoutBoxSizes() { |
| if (layout_boxes_) { |
| layout_boxes_->InvalidateSizes(); |
| |
| for (auto& pseudo_element : pseudo_elements_) { |
| if (pseudo_element && pseudo_element->layout_boxes()) { |
| pseudo_element->layout_boxes()->InvalidateSizes(); |
| } |
| } |
| } |
| } |
| |
| void HTMLElement::InvalidateLayoutBoxCrossReferences() { |
| if (layout_boxes_) { |
| layout_boxes_->InvalidateCrossReferences(); |
| |
| for (auto& pseudo_element : pseudo_elements_) { |
| if (pseudo_element && pseudo_element->layout_boxes()) { |
| pseudo_element->layout_boxes()->InvalidateCrossReferences(); |
| } |
| } |
| } |
| } |
| |
| void HTMLElement::InvalidateLayoutBoxRenderTreeNodes() { |
| if (layout_boxes_) { |
| layout_boxes_->InvalidateRenderTreeNodes(); |
| |
| for (auto& pseudo_element : pseudo_elements_) { |
| if (pseudo_element && pseudo_element->layout_boxes()) { |
| pseudo_element->layout_boxes()->InvalidateRenderTreeNodes(); |
| } |
| } |
| } |
| } |
| |
| void HTMLElement::InvalidateLayoutBoxes() { |
| layout_boxes_.reset(); |
| for (auto& pseudo_element : pseudo_elements_) { |
| if (pseudo_element) { |
| pseudo_element->reset_layout_boxes(); |
| } |
| } |
| directionality_ = base::nullopt; |
| } |
| |
| void HTMLElement::OnUiNavBlur(SbTimeMonotonic time) { |
| if (node_document() && node_document()->ui_nav_focus_element() == this) { |
| if (node_document()->TrySetUiNavFocusElement(nullptr, time)) { |
| Blur(); |
| } |
| } |
| } |
| |
| void HTMLElement::OnUiNavFocus(SbTimeMonotonic time) { |
| // Suppress the focus event if this is already focused -- i.e. the HTMLElement |
| // initiated the focus change that resulted in this call to OnUiNavFocus. |
| if (node_document() && node_document()->ui_nav_focus_element() != this) { |
| if (node_document()->TrySetUiNavFocusElement(this, time)) { |
| Focus(); |
| } |
| } |
| } |
| |
| void HTMLElement::OnUiNavScroll(SbTimeMonotonic /* time */) { |
| Document* document = node_document(); |
| scoped_refptr<Window> window(document ? document->window() : nullptr); |
| DispatchEvent(new UIEvent(base::Tokens::scroll(), web::Event::kNotBubbles, |
| web::Event::kNotCancelable, window)); |
| } |
| |
| HTMLElement::HTMLElement(Document* document, base::Token local_name) |
| : Element(document, local_name), |
| dom_stat_tracker_(document->html_element_context()->dom_stat_tracker()), |
| locked_for_focus_(false), |
| dir_(kDirNotDefined), |
| style_(new cssom::CSSDeclaredStyleDeclaration( |
| document->html_element_context()->css_parser())), |
| computed_style_valid_(false), |
| pseudo_elements_computed_styles_valid_(false), |
| descendant_computed_styles_valid_(false), |
| ancestors_are_displayed_(kAncestorsAreDisplayed), |
| css_computed_style_declaration_(new cssom::CSSComputedStyleDeclaration()), |
| ALLOW_THIS_IN_INITIALIZER_LIST( |
| transitions_adapter_(new DOMAnimatable(this))), |
| css_transitions_(&transitions_adapter_), |
| ALLOW_THIS_IN_INITIALIZER_LIST( |
| animations_adapter_(new DOMAnimatable(this))), |
| css_animations_(&animations_adapter_), |
| matching_rules_valid_(false) { |
| css_computed_style_declaration_->set_animations(animations()); |
| style_->set_mutation_observer(this); |
| dom_stat_tracker_->OnHtmlElementCreated(); |
| } |
| |
| HTMLElement::~HTMLElement() { |
| ReleaseUiNavigationItem(); |
| |
| if (IsInDocument()) { |
| dom_stat_tracker_->OnHtmlElementRemovedFromDocument(); |
| } |
| dom_stat_tracker_->OnHtmlElementDestroyed(); |
| |
| style_->set_mutation_observer(NULL); |
| } |
| |
| void HTMLElement::OnInsertedIntoDocument() { |
| Node::OnInsertedIntoDocument(); |
| dom_stat_tracker_->OnHtmlElementInsertedIntoDocument(); |
| } |
| |
| void HTMLElement::OnRemovedFromDocument() { |
| Node::OnRemovedFromDocument(); |
| dom_stat_tracker_->OnHtmlElementRemovedFromDocument(); |
| |
| // When an element that is focused stops being a focusable element, or stops |
| // being focused without another element being explicitly focused in its |
| // stead, the user agent should synchronously run the unfocusing steps for the |
| // affected element only. |
| // For example, this might happen because the element is removed from its |
| // Document, or has a hidden attribute added. It would also happen to an input |
| // element when the element gets disabled. |
| // https://www.w3.org/TR/html50/editing.html#unfocusing-steps |
| Document* document = node_document(); |
| DCHECK(document); |
| if (document->active_element() == this->AsElement()) { |
| RunUnFocusingSteps(); |
| document->OnFocusChange(); |
| } |
| |
| // Only clear the rule matching on this element. Descendants will be handled |
| // by OnRemovedFromDocument() being called on them from |
| // Node::OnRemovedFromDocument(). |
| ClearRuleMatchingStateInternal(false /*invalidate_descendants*/); |
| |
| ReleaseUiNavigationItem(); |
| } |
| |
| void HTMLElement::OnMutation() { InvalidateMatchingRulesRecursively(); } |
| |
| void HTMLElement::OnSetAttribute(const std::string& name, |
| const std::string& value) { |
| // Be sure to update HTMLElement::Duplicate() to copy over values as needed. |
| if (name == "dir") { |
| SetDir(value); |
| } else if (name == "tabindex") { |
| SetTabIndex(value); |
| } else if (name == kUiNavFocusDurationAttribute) { |
| SetUiNavFocusDuration(value); |
| } |
| |
| // Always clear the matching state when an attribute changes. Any attribute |
| // changing can potentially impact the matching rules. |
| ClearRuleMatchingState(); |
| } |
| |
| void HTMLElement::OnRemoveAttribute(const std::string& name) { |
| if (name == "dir") { |
| SetDir(""); |
| } else if (name == "tabindex") { |
| SetTabIndex(""); |
| } else if (name == kUiNavFocusDurationAttribute) { |
| SetUiNavFocusDuration(""); |
| } |
| |
| // Always clear the matching state when an attribute changes. Any attribute |
| // changing can potentially impact the matching rules. |
| ClearRuleMatchingState(); |
| } |
| |
| // Algorithm for IsFocusable: |
| // https://www.w3.org/TR/html50/editing.html#focusable |
| bool HTMLElement::IsFocusable() { |
| return HasTabindexFocusFlag() && IsBeingRendered(); |
| } |
| |
| // Algorithm for HasTabindexFocusFlag: |
| // https://www.w3.org/TR/html50/editing.html#specially-focusable |
| bool HTMLElement::HasTabindexFocusFlag() const { return tabindex_.has_value(); } |
| |
| // An element is being rendered if it has any associated CSS layout boxes, SVG |
| // layout boxes, or some equivalent in other styling languages. |
| // https://www.w3.org/TR/html50/rendering.html#being-rendered |
| bool HTMLElement::IsBeingRendered() { |
| Document* document = node_document(); |
| if (!document) { |
| return false; |
| } |
| |
| if (!document->UpdateComputedStyleOnElementAndAncestor(this)) { |
| return false; |
| } |
| DCHECK(computed_style()); |
| |
| return IsDisplayed() && |
| computed_style()->visibility() == cssom::KeywordValue::GetVisible(); |
| } |
| |
| // Algorithm for RunFocusingSteps: |
| // https://www.w3.org/TR/html50/editing.html#focusing-steps |
| void HTMLElement::RunFocusingSteps() { |
| // 1. If the element is not in a Document, or if the element's Document has |
| // no browsing context, or if the element's Document's browsing context has no |
| // top-level browsing context, or if the element is not focusable, or if the |
| // element is already focused, then abort these steps. |
| Document* document = node_document(); |
| if (!document || !document->HasBrowsingContext() || !IsFocusable()) { |
| return; |
| } |
| Element* old_active_element = document->active_element(); |
| if (old_active_element == this) { |
| return; |
| } |
| |
| // 2. If focusing the element will remove the focus from another element, |
| // then run the unfocusing steps for that element. |
| if (old_active_element && old_active_element->AsHTMLElement()) { |
| old_active_element->AsHTMLElement()->RunUnFocusingSteps(); |
| } |
| |
| // focusin: A user agent MUST dispatch this event when an event target is |
| // about to receive focus. This event type MUST be dispatched before the |
| // element is given focus. The event target MUST be the element which is about |
| // to receive focus. This event type is similar to focus, but is dispatched |
| // before focus is shifted, and does bubble. |
| // https://www.w3.org/TR/2016/WD-uievents-20160804/#event-type-focusin |
| DispatchEvent(new FocusEvent(base::Tokens::focusin(), web::Event::kBubbles, |
| web::Event::kNotCancelable, document->window(), |
| this)); |
| |
| // 3. Make the element the currently focused element in its top-level browsing |
| // context. |
| document->SetActiveElement(this); |
| |
| // 4. Not needed by Cobalt. |
| |
| // 5. Fire a simple event named focus at the element. |
| // focus: A user agent MUST dispatch this event when an event target receives |
| // focus. The focus MUST be given to the element before the dispatch of this |
| // event type. This event type is similar to focusin, but is dispatched after |
| // focus is shifted, and does not bubble. |
| // https://www.w3.org/TR/2016/WD-uievents-20160804/#event-type-focus |
| DispatchEvent(new FocusEvent(base::Tokens::focus(), web::Event::kNotBubbles, |
| web::Event::kNotCancelable, document->window(), |
| this)); |
| |
| // Custom, not in any spec. |
| ClearRuleMatchingState(); |
| } |
| |
| // Algorithm for RunUnFocusingSteps: |
| // https://www.w3.org/TR/html50/editing.html#unfocusing-steps |
| void HTMLElement::RunUnFocusingSteps() { |
| // 1. Not needed by Cobalt. |
| |
| // focusout: A user agent MUST dispatch this event when an event target is |
| // about to lose focus. This event type MUST be dispatched before the element |
| // loses focus. The event target MUST be the element which is about to lose |
| // focus. This event type is similar to blur, but is dispatched before focus |
| // is shifted, and does bubble. |
| // https://www.w3.org/TR/2016/WD-uievents-20160804/#event-type-focusout |
| Document* document = node_document(); |
| scoped_refptr<Window> window(document ? document->window() : NULL); |
| DispatchEvent(new FocusEvent(base::Tokens::focusout(), web::Event::kBubbles, |
| web::Event::kNotCancelable, window, this)); |
| |
| // 2. Unfocus the element. |
| if (document && document->active_element() == this->AsElement()) { |
| document->SetActiveElement(NULL); |
| } |
| |
| // 3. Fire a simple event named blur at the element. |
| // blur: A user agent MUST dispatch this event when an event target loses |
| // focus. The focus MUST be taken from the element before the dispatch of this |
| // event type. This event type is similar to focusout, but is dispatched after |
| // focus is shifted, and does not bubble. |
| // https://www.w3.org/TR/2016/WD-uievents-20160804/#event-type-blur |
| DispatchEvent(new FocusEvent(base::Tokens::blur(), web::Event::kNotBubbles, |
| web::Event::kNotCancelable, document->window(), |
| this)); |
| |
| // Custom, not in any spec. |
| ClearRuleMatchingState(); |
| } |
| |
| void HTMLElement::UpdateUiNavigationFocus() { |
| if (!node_document()) { |
| return; |
| } |
| |
| // Set the focus item for the UI navigation system. Search up the DOM tree to |
| // find the nearest ancestor that is a UI navigation item if needed. Do this |
| // step before dispatching events as the event handlers may make UI navigation |
| // changes. |
| for (Node* node = this; node; node = node->parent_node()) { |
| Element* element = node->AsElement(); |
| if (!element) { |
| continue; |
| } |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (!html_element) { |
| continue; |
| } |
| if (!html_element->ui_nav_item_ || |
| html_element->ui_nav_item_->IsContainer()) { |
| continue; |
| } |
| |
| // Updating the UI navigation focus element has the additional effect of |
| // suppressing the Blur call for the previously focused HTMLElement and the |
| // Focus call for this HTMLElement as a result of OnUiNavBlur / OnUiNavFocus |
| // callbacks that result from initiating the UI navigation focus change. |
| if (node_document()->TrySetUiNavFocusElement(html_element, |
| SbTimeGetMonotonicNow())) { |
| html_element->ui_nav_item_->Focus(); |
| } |
| break; |
| } |
| } |
| |
| void HTMLElement::SetUiNavItemBounds() { |
| if (!ui_nav_item_->IsContainer()) { |
| return; |
| } |
| |
| DirState dir = GetUsedDirState(); |
| if (dir == DirState::kDirNotDefined) { |
| Document* document = node_document(); |
| if (document && document->html()) { |
| dir = document->html()->GetUsedDirState(); |
| } |
| } |
| |
| float scrollable_width = scroll_width() - client_width(); |
| float scroll_top_lower_bound = 0.0f; |
| float scroll_left_lower_bound = |
| dir == DirState::kDirRightToLeft ? -scrollable_width : 0.0f; |
| float scroll_top_upper_bound = scroll_height() - client_height(); |
| float scroll_left_upper_bound = |
| dir == DirState::kDirRightToLeft ? 0.0f : scrollable_width; |
| ui_nav_item_->SetBounds(scroll_top_lower_bound, scroll_left_lower_bound, |
| scroll_top_upper_bound, scroll_left_upper_bound); |
| } |
| |
| void HTMLElement::SetDir(const std::string& value) { |
| // https://html.spec.whatwg.org/commit-snapshots/ebcac971c2add28a911283899da84ec509876c44/#the-dir-attribute |
| auto previous_dir = dir_; |
| if (value == "auto") { |
| dir_ = kDirAuto; |
| } else if (value == "ltr") { |
| dir_ = kDirLeftToRight; |
| } else if (value == "rtl") { |
| dir_ = kDirRightToLeft; |
| } else { |
| dir_ = kDirNotDefined; |
| |
| // Reset the attribute so that element.getAttribute('dir') returns the |
| // same thing as element.dir. |
| if (value.size() > 0) { |
| LOG(WARNING) << "Unsupported value '" << value << "' for attribute 'dir'"; |
| SetAttribute("dir", ""); |
| } |
| } |
| |
| if (dir_ != previous_dir) { |
| InvalidateLayoutBoxesOfNodeAndAncestors(); |
| InvalidateLayoutBoxesOfDescendants(); |
| } |
| } |
| |
| void HTMLElement::SetTabIndex(const std::string& value) { |
| int32 tabindex; |
| if (base::StringToInt32(value, &tabindex)) { |
| tabindex_ = tabindex; |
| } else { |
| tabindex_ = base::nullopt; |
| } |
| |
| // Changing the tabindex may trigger a UI navigation change. |
| ui_nav_needs_update_ = true; |
| } |
| |
| void HTMLElement::SetUiNavFocusDuration(const std::string& value) { |
| double duration; |
| if (base::StringToDouble(value, &duration)) { |
| ui_nav_focus_duration_ = static_cast<float>(duration); |
| if (!std::isfinite(*ui_nav_focus_duration_) || |
| *ui_nav_focus_duration_ < 0.0f) { |
| ui_nav_focus_duration_ = 0.0f; |
| } |
| if (ui_nav_item_) { |
| ui_nav_item_->SetFocusDuration(*ui_nav_focus_duration_); |
| } |
| } else { |
| ui_nav_focus_duration_ = base::nullopt; |
| if (ui_nav_item_) { |
| ui_nav_item_->SetFocusDuration(0.0f); |
| } |
| } |
| } |
| |
| namespace { |
| // This is similar to base rtl.h's GetStringDirection; however, this takes a |
| // utf8 string and only pays attention to L, AL, and R character types. |
| HTMLElement::DirState GetStringDirection(const std::string& utf8_string) { |
| int32_t length = static_cast<int32_t>(utf8_string.length()); |
| for (int32_t index = 0; index < length;) { |
| int32_t ch; |
| U8_NEXT(utf8_string.data(), index, length, ch); |
| if (ch < 0) { |
| LOG(ERROR) << "Unable to determine directionality of " << utf8_string; |
| break; |
| } |
| |
| int32_t property = u_getIntPropertyValue(ch, UCHAR_BIDI_CLASS); |
| if (property == U_LEFT_TO_RIGHT) { |
| return HTMLElement::kDirLeftToRight; |
| } |
| if (property == U_RIGHT_TO_LEFT || property == U_RIGHT_TO_LEFT_ARABIC) { |
| return HTMLElement::kDirRightToLeft; |
| } |
| } |
| return HTMLElement::kDirNotDefined; |
| } |
| } // namespace |
| |
| // This is similar to dir_state() except it will resolve kDirAuto to |
| // kDirLeftToRight or kDirRightToLeft according to the spec: |
| // https://html.spec.whatwg.org/commit-snapshots/ebcac971c2add28a911283899da84ec509876c44/#the-directionality |
| // If "dir" was not defined for this element, then this function will return |
| // kDirNotDefined. |
| HTMLElement::DirState HTMLElement::GetUsedDirState() { |
| // If the element's dir attribute is in the auto state |
| // If the element is a bdi element and the dir attribute is not in a defined |
| // state (i.e. it is not present or has an invalid value) |
| if (dir_ != kDirAuto) { |
| return dir_; |
| } |
| |
| // Find the first character in tree order that matches the following criteria: |
| // The character is from a Text node that is a descendant of the element |
| // whose directionality is being determined. |
| // The character is of bidirectional character type L, AL, or R. [BIDI] |
| // The character is not in a Text node that has an ancestor element that is |
| // a descendant of the element whose directionality is being determined |
| // and that is either: |
| // A bdi element. |
| // A script element. |
| // A style element. |
| // A textarea element. |
| // An element with a dir attribute in a defined state. |
| // If such a character is found and it is of bidirectional character type |
| // AL or R, the directionality of the element is 'rtl'. |
| // If such a character is found and it is of bidirectional character type |
| // L, the directionality of the element is 'ltr'. |
| |
| // A tree is a finite hierarchical tree structure. In tree order is preorder, |
| // depth-first traversal of a tree. |
| // https://dom.spec.whatwg.org/#concept-tree-order |
| std::vector<Node*> stack; |
| |
| // Add children in reverse order so that pop_back() will result in preorder |
| // depth-first traversal. |
| for (Node* child_node = last_child(); child_node; |
| child_node = child_node->previous_sibling()) { |
| stack.push_back(child_node); |
| } |
| |
| while (!stack.empty()) { |
| Node* node = stack.back(); |
| stack.pop_back(); |
| |
| Text* text = node->AsText(); |
| if (text) { |
| // If the text has strong directionality, then return it. |
| DirState dir = GetStringDirection(text->text()); |
| if (dir != kDirNotDefined) { |
| return dir; |
| } |
| } |
| |
| // Traverse children only if this is not: |
| // A bdi element. |
| // A script element. |
| // A style element. |
| // A textarea element. |
| // An element with a dir attribute in a defined state. |
| Element* element = node->AsElement(); |
| if (element) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| if (html_element->AsHTMLScriptElement() || |
| html_element->AsHTMLStyleElement() || |
| html_element->dir_state() != kDirNotDefined) { |
| continue; |
| } |
| } |
| } |
| |
| for (Node* child_node = node->last_child(); child_node; |
| child_node = child_node->previous_sibling()) { |
| stack.push_back(child_node); |
| } |
| } |
| |
| // Otherwise, if the element is a document element, the directionality of |
| // the element is 'ltr'. |
| if (IsDocumentElement()) { |
| return kDirLeftToRight; |
| } |
| |
| // Although the spec says to use the parent's directionality, the W3C test |
| // (the-dir-attribute-069.html) says to default to LTR. Chrome follows the |
| // W3C expectation, so follow Chrome. Additional discussion here: |
| // https://github.com/w3c/i18n-drafts/issues/235 |
| // The following code block which implements the spec is left for reference. |
| #if 0 |
| // Otherwise, the directionality of the element is the same as the element's |
| // parent element's directionality. |
| for (Node* ancestor_node = parent_node(); ancestor_node; |
| ancestor_node = ancestor_node->parent_node()) { |
| Element* ancestor_element = ancestor_node->AsElement(); |
| if (!ancestor_element) { |
| continue; |
| } |
| HTMLElement* ancestor_html_element = ancestor_element->AsHTMLElement(); |
| if (!ancestor_html_element) { |
| continue; |
| } |
| if (ancestor_html_element->dir_state() == kDirNotDefined) { |
| continue; |
| } |
| return ancestor_html_element->GetUsedDirState(); |
| } |
| #endif |
| |
| return kDirLeftToRight; |
| } |
| |
| // Algorithm: |
| // https://html.spec.whatwg.org/commit-snapshots/ebcac971c2add28a911283899da84ec509876c44/#the-directionality |
| Directionality HTMLElement::directionality() { |
| // Use the cached value if available. |
| if (directionality_) { |
| return *directionality_; |
| } |
| |
| // The directionality of an element (any element, not just an HTML element) |
| // is either 'ltr' or 'rtl', and is determined as per the first appropriate |
| // set of steps from the following list: |
| |
| // If the element's dir attribute is in the ltr state |
| // If the element is a document element and the dir attribute is not in a |
| // defined state (i.e. it is not present or has an invalid value) |
| // If the element is an input element whose type attribute is in the |
| // Telephone state, and the dir attribute is not in a defined state (i.e. |
| // it is not present or has an invalid value) |
| // --> The directionality of the element is 'ltr'. |
| if (dir_ == kDirLeftToRight) { |
| directionality_ = kLeftToRightDirectionality; |
| return *directionality_; |
| } |
| // [Case of undefined 'dir' is handled later in this function.] |
| |
| // If the element's dir attribute is in the rtl state |
| // --> The directionality of the element is 'rtl'. |
| if (dir_ == kDirRightToLeft) { |
| directionality_ = kRightToLeftDirectionality; |
| return *directionality_; |
| } |
| |
| // If the element is an input element whose type attribute is in the Text, |
| // Search, Telephone, URL, or E-mail state, and the dir attribute is in the |
| // auto state |
| // If the element is a textarea element and the dir attribute is in the auto |
| // state |
| // --> Cobalt does not support these element types. |
| |
| // If the element's dir attribute is in the auto state |
| // If the element is a bdi element and the dir attribute is not in a defined |
| // state (i.e. it is not present or has an invalid value) |
| // --> Find the first character in tree order that matches the following |
| // criteria: |
| // The character is from a Text node that is a descendant of the |
| // element whose directionality is being determined. |
| // The character is of bidirectional character type L, AL, or R. [BIDI] |
| // The character is not in a Text node that has an ancestor element |
| // that is a descendant of the element whose directionality is being |
| // determined and that is either: |
| // A bdi element. |
| // A script element. |
| // A style element. |
| // A textarea element. |
| // An element with a dir attribute in a defined state. |
| // If such a character is found and it is of bidirectional character type |
| // AL or R, the directionality of the element is 'rtl'. |
| // If such a character is found and it is of bidirectional character type |
| // L, the directionality of the element is 'ltr'. |
| // Otherwise, if the element is a document element, the directionality of |
| // the element is 'ltr'. |
| // Otherwise, the directionality of the element is the same as the |
| // element's parent element's directionality. |
| |
| // If the element has a parent element and the dir attribute is not in a |
| // defined state (i.e. it is not present or has an invalid value) |
| // --> The directionality of the element is the same as the element's parent |
| // element's directionality. |
| for (Node* ancestor_node = parent_node(); ancestor_node; |
| ancestor_node = ancestor_node->parent_node()) { |
| Element* ancestor_element = ancestor_node->AsElement(); |
| if (!ancestor_element) { |
| continue; |
| } |
| HTMLElement* ancestor_html_element = ancestor_element->AsHTMLElement(); |
| if (!ancestor_html_element) { |
| continue; |
| } |
| directionality_ = ancestor_html_element->directionality(); |
| return *directionality_; |
| } |
| |
| directionality_ = kLeftToRightDirectionality; |
| return *directionality_; |
| } |
| |
| namespace { |
| |
| scoped_refptr<cssom::CSSComputedStyleData> PromoteMatchingRulesToComputedStyle( |
| cssom::RulesWithCascadePrecedence* matching_rules, |
| cssom::GURLMap* property_key_to_base_url_map, |
| const scoped_refptr<const cssom::CSSDeclaredStyleData>& inline_style, |
| const scoped_refptr<cssom::CSSComputedStyleDeclaration>& |
| parent_computed_style_declaration, |
| const scoped_refptr<const cssom::CSSComputedStyleData>& root_computed_style, |
| const ViewportSize& viewport_size) { |
| // Select the winning value for each property by performing the cascade, |
| // that is, apply values from matching rules on top of inline style, taking |
| // into account rule specificity and location in the source file, as well as |
| // property declaration importance. |
| scoped_refptr<cssom::MutableCSSComputedStyleData> computed_style = |
| PromoteToCascadedStyle(inline_style, matching_rules, |
| property_key_to_base_url_map); |
| |
| // Lastly, absolutize the values, if possible. Start by resolving "initial" |
| // and "inherit" keywords (which gives us what the specification refers to |
| // as "specified style"). Then, convert length units and percentages into |
| // pixels, convert color keywords into RGB triplets, and so on. For certain |
| // properties, like "font-family", computed value is the same as specified |
| // value. Declarations that cannot be absolutized easily, like "width: auto;", |
| // will be resolved during layout. |
| cssom::PromoteToComputedStyle( |
| computed_style, parent_computed_style_declaration, root_computed_style, |
| viewport_size.width_height(), property_key_to_base_url_map); |
| |
| return computed_style; |
| } |
| |
| void PossiblyActivateAnimations( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& |
| previous_computed_style, |
| const scoped_refptr<const cssom::CSSComputedStyleData>& new_computed_style, |
| const base::TimeDelta& style_change_event_time, |
| cssom::TransitionSet* css_transitions, cssom::AnimationSet* css_animations, |
| const cssom::CSSKeyframesRule::NameMap& keyframes_map, |
| HTMLElement::AncestorsAreDisplayed old_ancestors_are_displayed, |
| HTMLElement::AncestorsAreDisplayed new_ancestors_are_displayed, |
| bool* animations_modified) { |
| // Calculate some helper values to help determine if we are transitioning from |
| // not being displayed to being displayed, or vice-versa. Animations should |
| // not be playing if we are not being displayed. |
| bool old_is_displayed = |
| old_ancestors_are_displayed == HTMLElement::kAncestorsAreDisplayed && |
| (previous_computed_style && |
| previous_computed_style->display() != cssom::KeywordValue::GetNone()); |
| bool new_is_displayed = |
| new_ancestors_are_displayed == HTMLElement::kAncestorsAreDisplayed && |
| new_computed_style->display() != cssom::KeywordValue::GetNone(); |
| |
| if (new_is_displayed) { |
| // Don't start any transitions if we are transitioning from display: none. |
| if (previous_computed_style && old_is_displayed) { |
| // Now that we have updated our computed style, compare it to the previous |
| // style and see if we need to adjust our animations. |
| css_transitions->UpdateTransitions(style_change_event_time, |
| *previous_computed_style, |
| *new_computed_style); |
| } |
| // Update the set of currently running animations and track whether or not |
| // the animations changed. |
| *animations_modified = css_animations->Update( |
| style_change_event_time, *new_computed_style, keyframes_map); |
| } else { |
| if (old_is_displayed) { |
| css_transitions->Clear(); |
| css_animations->Clear(); |
| } else { |
| DCHECK(css_transitions->empty()); |
| DCHECK(css_animations->empty()); |
| } |
| } |
| } |
| |
| // Flags tracking which cached values must be invalidated. |
| struct UpdateComputedStyleInvalidationFlags { |
| UpdateComputedStyleInvalidationFlags() |
| : mark_descendants_as_display_none(false), |
| invalidate_computed_styles_of_descendants(false), |
| invalidate_layout_boxes(false), |
| invalidate_sizes(false), |
| invalidate_cross_references(false), |
| invalidate_render_tree_nodes(false) {} |
| |
| bool mark_descendants_as_display_none; |
| bool invalidate_computed_styles_of_descendants; |
| bool invalidate_layout_boxes; |
| bool invalidate_sizes; |
| bool invalidate_cross_references; |
| bool invalidate_render_tree_nodes; |
| }; |
| |
| bool NewComputedStyleMarksDescendantsAsDisplayNone( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& old_computed_style, |
| const scoped_refptr<cssom::CSSComputedStyleData>& new_computed_style) { |
| return old_computed_style->display() != cssom::KeywordValue::GetNone() && |
| new_computed_style->display() == cssom::KeywordValue::GetNone(); |
| } |
| |
| bool NewComputedStyleInvalidatesComputedStylesOfDescendants( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& old_computed_style, |
| const scoped_refptr<cssom::CSSComputedStyleData>& new_computed_style) { |
| return !non_trivial_static_fields.Get() |
| .computed_style_invalidation_property_checker |
| .DoDeclaredPropertiesMatch(old_computed_style, |
| new_computed_style); |
| } |
| |
| bool NewComputedStyleInvalidatesLayoutBoxes( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& old_computed_style, |
| const scoped_refptr<cssom::CSSComputedStyleData>& new_computed_style) { |
| return !non_trivial_static_fields.Get() |
| .layout_box_invalidation_property_checker |
| .DoDeclaredPropertiesMatch(old_computed_style, |
| new_computed_style); |
| } |
| |
| bool NewComputedStyleInvalidatesSizes( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& old_computed_style, |
| const scoped_refptr<cssom::CSSComputedStyleData>& new_computed_style) { |
| return !non_trivial_static_fields.Get() |
| .size_invalidation_property_checker.DoDeclaredPropertiesMatch( |
| old_computed_style, new_computed_style); |
| } |
| |
| bool NewComputedStyleInvalidatesCrossReferences( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& old_computed_style, |
| const scoped_refptr<cssom::CSSComputedStyleData>& new_computed_style) { |
| return !non_trivial_static_fields.Get() |
| .cross_references_invalidation_property_checker |
| .DoDeclaredPropertiesMatch(old_computed_style, |
| new_computed_style); |
| } |
| |
| enum IsPseudoElement { |
| kIsNotPseudoElement, |
| kIsPseudoElement, |
| }; |
| |
| void UpdateInvalidationFlagsFromNewComputedStyle( |
| const scoped_refptr<const cssom::CSSComputedStyleData>& old_computed_style, |
| const scoped_refptr<cssom::CSSComputedStyleData>& new_computed_style, |
| bool animations_modified, IsPseudoElement is_pseudo_element, |
| UpdateComputedStyleInvalidationFlags* flags) { |
| if (old_computed_style) { |
| if (!flags->mark_descendants_as_display_none && |
| is_pseudo_element == kIsNotPseudoElement && |
| NewComputedStyleMarksDescendantsAsDisplayNone(old_computed_style, |
| new_computed_style)) { |
| flags->mark_descendants_as_display_none = true; |
| } |
| if (!flags->invalidate_computed_styles_of_descendants && |
| NewComputedStyleInvalidatesComputedStylesOfDescendants( |
| old_computed_style, new_computed_style)) { |
| flags->invalidate_computed_styles_of_descendants = true; |
| flags->invalidate_layout_boxes = true; |
| } else if (!flags->invalidate_layout_boxes) { |
| if (NewComputedStyleInvalidatesLayoutBoxes(old_computed_style, |
| new_computed_style)) { |
| flags->invalidate_layout_boxes = true; |
| } else { |
| if (!flags->invalidate_sizes && |
| NewComputedStyleInvalidatesSizes(old_computed_style, |
| new_computed_style)) { |
| flags->invalidate_sizes = true; |
| flags->invalidate_render_tree_nodes = true; |
| } |
| if (!flags->invalidate_cross_references && |
| NewComputedStyleInvalidatesCrossReferences(old_computed_style, |
| new_computed_style)) { |
| flags->invalidate_cross_references = true; |
| flags->invalidate_render_tree_nodes = true; |
| } |
| |
| flags->invalidate_render_tree_nodes = |
| flags->invalidate_render_tree_nodes || animations_modified || |
| !new_computed_style->DoDeclaredPropertiesMatch(old_computed_style); |
| } |
| } |
| } |
| } |
| |
| void DoComputedStyleUpdate( |
| cssom::RulesWithCascadePrecedence* matching_rules, |
| cssom::GURLMap* property_key_to_base_url_map, |
| const scoped_refptr<const cssom::CSSDeclaredStyleData>& inline_style, |
| const scoped_refptr<cssom::CSSComputedStyleDeclaration>& |
| parent_computed_style_declaration, |
| const scoped_refptr<const cssom::CSSComputedStyleData>& root_computed_style, |
| const ViewportSize& viewport_size, |
| const scoped_refptr<const cssom::CSSComputedStyleData>& |
| previous_computed_style, |
| const base::TimeDelta& style_change_event_time, |
| cssom::TransitionSet* css_transitions, cssom::AnimationSet* css_animations, |
| const cssom::CSSKeyframesRule::NameMap& keyframes_map, |
| HTMLElement::AncestorsAreDisplayed old_ancestors_are_displayed, |
| HTMLElement::AncestorsAreDisplayed new_ancestors_are_displayed, |
| IsPseudoElement is_pseudo_element, |
| UpdateComputedStyleInvalidationFlags* invalidation_flags, |
| cssom::CSSComputedStyleDeclaration* css_computed_style_declaration) { |
| bool animations_modified = false; |
| |
| scoped_refptr<cssom::CSSComputedStyleData> new_computed_style = |
| PromoteMatchingRulesToComputedStyle( |
| matching_rules, property_key_to_base_url_map, inline_style, |
| parent_computed_style_declaration, root_computed_style, |
| viewport_size); |
| |
| PossiblyActivateAnimations(previous_computed_style, new_computed_style, |
| style_change_event_time, css_transitions, |
| css_animations, keyframes_map, |
| old_ancestors_are_displayed, |
| new_ancestors_are_displayed, &animations_modified); |
| |
| UpdateInvalidationFlagsFromNewComputedStyle( |
| previous_computed_style, new_computed_style, animations_modified, |
| is_pseudo_element, invalidation_flags); |
| |
| css_computed_style_declaration->SetData(new_computed_style); |
| } |
| } // namespace |
| |
| void HTMLElement::UpdateComputedStyle( |
| const scoped_refptr<cssom::CSSComputedStyleDeclaration>& |
| parent_computed_style_declaration, |
| const scoped_refptr<const cssom::CSSComputedStyleData>& root_computed_style, |
| const base::TimeDelta& style_change_event_time, |
| AncestorsAreDisplayed ancestors_are_displayed) { |
| Document* document = node_document(); |
| DCHECK(document) << "Element should be attached to document in order to " |
| "participate in layout."; |
| |
| // Verify that the matching rules for this element are valid. They should have |
| // been updated prior to UpdateComputedStyle() being called. |
| DCHECK(matching_rules_valid_); |
| // If there is no previous computed style, there should also be no layout |
| // boxes. |
| DCHECK(computed_style() || NULL == layout_boxes()); |
| |
| dom_stat_tracker_->OnUpdateComputedStyle(); |
| |
| // The computed style must be generated if either the computed style is |
| // invalid or no computed style has been created yet. |
| bool generate_computed_style = !computed_style_valid_ || !computed_style(); |
| |
| // If any declared properties inherited from the parent are no longer valid, |
| // then a new computed style must be generated with the updated inherited |
| // values. |
| if (!generate_computed_style && |
| !computed_style()->AreDeclaredPropertiesInheritedFromParentValid()) { |
| generate_computed_style = true; |
| } |
| |
| // It is possible for computed style to have been updated on this element even |
| // if its ancestors were set to display: none. If this has changed, we would |
| // need to update our computed style again, even if nothing else has changed. |
| if (!generate_computed_style && |
| ancestors_are_displayed_ == kAncestorsAreNotDisplayed && |
| ancestors_are_displayed == kAncestorsAreDisplayed) { |
| generate_computed_style = true; |
| } |
| |
| // TODO: It maybe helpful to generalize this mapping framework in the |
| // future to allow more data and context about where a cssom::PropertyValue |
| // came from. |
| cssom::GURLMap property_key_to_base_url_map; |
| property_key_to_base_url_map[cssom::kBackgroundImageProperty] = |
| document->location()->url(); |
| |
| // Flags tracking which cached values must be invalidated. |
| UpdateComputedStyleInvalidationFlags invalidation_flags; |
| |
| // We record this now before we make changes to the computed style and use |
| // it later for the pseudo element computed style updates. |
| bool old_is_displayed = computed_style() && IsDisplayed(); |
| |
| if (generate_computed_style) { |
| dom_stat_tracker_->OnGenerateHtmlElementComputedStyle(); |
| DoComputedStyleUpdate( |
| matching_rules(), &property_key_to_base_url_map, style_->data(), |
| parent_computed_style_declaration, root_computed_style, |
| document->viewport_size(), computed_style(), style_change_event_time, |
| &css_transitions_, &css_animations_, document->keyframes_map(), |
| ancestors_are_displayed_, ancestors_are_displayed, kIsNotPseudoElement, |
| &invalidation_flags, css_computed_style_declaration_); |
| |
| // Update cached background images after resolving the urls in |
| // background_image CSS property of the computed style, so we have all the |
| // information to get the cached background images. |
| UpdateCachedBackgroundImagesFromComputedStyle(); |
| } else { |
| // Update the inherited data if a new style was not generated. The ancestor |
| // data with inherited properties may have changed. |
| css_computed_style_declaration_->UpdateInheritedData(); |
| } |
| |
| // Update the displayed status of our ancestors. |
| ancestors_are_displayed_ = ancestors_are_displayed; |
| |
| // Process pseudo elements. They must have their computed style generated if |
| // either their owning HTML element's style was just generated or their |
| // computed style is invalid (this occurs when their matching rules change). |
| for (int type = 0; type < kMaxPseudoElementType; ++type) { |
| PseudoElement* type_pseudo_element = |
| pseudo_element(PseudoElementType(type)); |
| if (type_pseudo_element) { |
| if (generate_computed_style || |
| type_pseudo_element->computed_style_invalid()) { |
| dom_stat_tracker_->OnGeneratePseudoElementComputedStyle(); |
| DoComputedStyleUpdate( |
| type_pseudo_element->matching_rules(), |
| &property_key_to_base_url_map, NULL, |
| css_computed_style_declaration(), root_computed_style, |
| document->viewport_size(), type_pseudo_element->computed_style(), |
| style_change_event_time, type_pseudo_element->css_transitions(), |
| type_pseudo_element->css_animations(), document->keyframes_map(), |
| old_is_displayed ? kAncestorsAreDisplayed |
| : kAncestorsAreNotDisplayed, |
| IsDisplayed() ? kAncestorsAreDisplayed : kAncestorsAreNotDisplayed, |
| kIsPseudoElement, &invalidation_flags, |
| type_pseudo_element->css_computed_style_declaration()); |
| type_pseudo_element->clear_computed_style_invalid(); |
| } else { |
| // Update the inherited data if a new style was not generated. The |
| // ancestor data with inherited properties may have changed. |
| type_pseudo_element->css_computed_style_declaration() |
| ->UpdateInheritedData(); |
| } |
| } |
| } |
| |
| if (invalidation_flags.mark_descendants_as_display_none) { |
| MarkNotDisplayedOnDescendants(); |
| } |
| if (invalidation_flags.invalidate_computed_styles_of_descendants) { |
| InvalidateComputedStylesOfDescendants(); |
| } |
| |
| if (invalidation_flags.invalidate_layout_boxes) { |
| InvalidateLayoutBoxesOfNodeAndAncestors(); |
| InvalidateLayoutBoxesOfDescendants(); |
| } else { |
| if (invalidation_flags.invalidate_sizes) { |
| InvalidateLayoutBoxSizes(); |
| } |
| if (invalidation_flags.invalidate_cross_references) { |
| InvalidateLayoutBoxCrossReferences(); |
| } |
| if (invalidation_flags.invalidate_render_tree_nodes) { |
| InvalidateLayoutBoxRenderTreeNodes(); |
| } |
| } |
| |
| computed_style_valid_ = true; |
| pseudo_elements_computed_styles_valid_ = true; |
| } |
| |
| void HTMLElement::CollectHTMLMediaElementsRecursively( |
| std::vector<HTMLMediaElement*>* html_media_elements, |
| int current_element_depth) { |
| int max_depth = node_document()->dom_max_element_depth(); |
| if (max_depth > 0 && current_element_depth >= max_depth) { |
| return; |
| } |
| |
| for (Element* element = first_element_child(); element; |
| element = element->next_element_sibling()) { |
| HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element) { |
| HTMLMediaElement* media_html_element = html_element->AsHTMLMediaElement(); |
| if (media_html_element) { |
| html_media_elements->push_back(media_html_element); |
| } |
| html_element->CollectHTMLMediaElementsRecursively( |
| html_media_elements, current_element_depth + 1); |
| } |
| } |
| } |
| |
| void HTMLElement::SetPseudoElement( |
| PseudoElementType type, std::unique_ptr<PseudoElement> pseudo_element) { |
| DCHECK_EQ(this, pseudo_element->parent_element()); |
| DCHECK(type < kMaxPseudoElementType); |
| pseudo_elements_[type] = std::move(pseudo_element); |
| pseudo_elements_computed_styles_valid_ = false; |
| } |
| |
| bool HTMLElement::AreComputedStylesValid() const { |
| return computed_style_valid_ && pseudo_elements_computed_styles_valid_; |
| } |
| |
| bool HTMLElement::IsDesignated() const { |
| Document* document = node_document(); |
| if (document) { |
| scoped_refptr<Element> element = document->indicated_element(); |
| while (element) { |
| if (element.get() == this) { |
| return true; |
| } |
| // The parent of an element that is :hover is also in that state. |
| // https://www.w3.org/TR/selectors4/#hover-pseudo |
| element = element->parent_element(); |
| } |
| } |
| return false; |
| } |
| |
| bool HTMLElement::CanBeDesignatedByPointerIfDisplayed() const { |
| return computed_style()->pointer_events() != cssom::KeywordValue::GetNone() && |
| computed_style()->visibility() == cssom::KeywordValue::GetVisible(); |
| } |
| |
| void HTMLElement::UpdateUiNavigation() { |
| ui_nav_needs_update_ = false; |
| |
| base::Optional<ui_navigation::NativeItemType> ui_nav_item_type; |
| if (tabindex_ && *tabindex_ <= kUiNavFocusTabIndexThreshold && |
| computed_style()->pointer_events() != cssom::KeywordValue::GetNone()) { |
| ui_nav_item_type = ui_navigation::kNativeItemTypeFocus; |
| } else if (computed_style()->overflow() == cssom::KeywordValue::GetAuto() || |
| computed_style()->overflow() == cssom::KeywordValue::GetScroll()) { |
| ui_nav_item_type = ui_navigation::kNativeItemTypeContainer; |
| } |
| |
| if (ui_nav_item_type && IsDisplayed() && node_document()) { |
| ui_navigation::NativeItemDir ui_nav_item_dir; |
| ui_nav_item_dir.is_left_to_right = |
| directionality() == kLeftToRightDirectionality; |
| ui_nav_item_dir.is_top_to_bottom = true; |
| |
| if (ui_nav_item_) { |
| if (ui_nav_item_->GetType() == *ui_nav_item_type) { |
| // Keep using the existing navigation item. |
| ui_nav_item_->SetDir(ui_nav_item_dir); |
| return; |
| } |
| // The current navigation item isn't of the correct type. Disable it so |
| // that callbacks won't be invoked for it. The object will be destroyed |
| // when all references to it are released. |
| ReleaseUiNavigationItem(); |
| } |
| |
| ui_nav_item_ = new ui_navigation::NavItem( |
| *ui_nav_item_type, |
| base::Bind( |
| &UiNavCallbackHelper, base::ThreadTaskRunnerHandle::Get(), |
| base::Bind(&HTMLElement::OnUiNavBlur, base::AsWeakPtr(this))), |
| base::Bind( |
| &UiNavCallbackHelper, base::ThreadTaskRunnerHandle::Get(), |
| base::Bind(&HTMLElement::OnUiNavFocus, base::AsWeakPtr(this))), |
| base::Bind( |
| &UiNavCallbackHelper, base::ThreadTaskRunnerHandle::Get(), |
| base::Bind(&HTMLElement::OnUiNavScroll, base::AsWeakPtr(this)))); |
| ui_nav_item_->SetDir(ui_nav_item_dir); |
| if (ui_nav_focus_duration_) { |
| ui_nav_item_->SetFocusDuration(*ui_nav_focus_duration_); |
| } |
| |
| node_document()->AddUiNavigationElement(this); |
| node_document()->set_ui_nav_needs_layout(true); |
| InvalidateLayoutBoxRenderTreeNodes(); |
| if (layout_boxes_) { |
| layout_boxes_->SetUiNavItem(ui_nav_item_); |
| } |
| } else if (ui_nav_item_) { |
| // This navigation item is no longer relevant. |
| ReleaseUiNavigationItem(); |
| InvalidateLayoutBoxRenderTreeNodes(); |
| if (layout_boxes_) { |
| layout_boxes_->SetUiNavItem(ui_nav_item_); |
| } |
| } |
| } |
| |
| void HTMLElement::ReleaseUiNavigationItem() { |
| if (ui_nav_item_) { |
| // Disable the UI navigation item so it won't receive anymore callbacks |
| // while being released. |
| if (node_document()) { |
| node_document()->RemoveUiNavigationElement(this); |
| node_document()->set_ui_nav_needs_layout(true); |
| if (node_document()->ui_nav_focus_element() == this) { |
| if (node_document()->TrySetUiNavFocusElement(nullptr, |
| SbTimeGetMonotonicNow())) { |
| ui_nav_item_->UnfocusAll(); |
| } |
| } |
| } |
| ui_nav_item_->SetEnabled(false); |
| ui_nav_item_ = nullptr; |
| } |
| } |
| |
| void HTMLElement::ClearActiveBackgroundImages() { |
| if (html_element_context() && |
| html_element_context()->animated_image_tracker()) { |
| for (std::vector<GURL>::iterator it = active_background_images_.begin(); |
| it != active_background_images_.end(); ++it) { |
| html_element_context()->animated_image_tracker()->DecreaseURLCount(*it); |
| } |
| } |
| active_background_images_.clear(); |
| } |
| |
| void HTMLElement::ClearCachedBackgroundImages() { |
| // |cached_background_images_.clear()| cannot be used as it may lead to crash |
| // due to GetLoadTimingInfoAndCreateResourceTiming() being called indirectly |
| // from CachedImage dtor, and GetLoadTimingInfoAndCreateResourceTiming() loops |
| // on the |cached_background_images_| being cleared. |
| // |
| // To move and clear() like below is more straight-forward but leads to more |
| // in flight image loading performance timings get lost. |
| // auto images = std::move(cached_background_images_); |
| // DCHECK(cached_background_images_.empty()); |
| // images.clear(); |
| // |
| // So images are moved out and cleared one by one. |
| while (!cached_background_images_.empty()) { |
| auto image = std::move(cached_background_images_.back()); |
| DCHECK(!cached_background_images_.back()); |
| cached_background_images_.pop_back(); |
| // TODO(b/265089478): This implementation will lose the performance timing |
| // of |image|, consider refining to record all performance timings. |
| image = nullptr; |
| } |
| } |
| |
| void HTMLElement::UpdateCachedBackgroundImagesFromComputedStyle() { |
| ClearActiveBackgroundImages(); |
| |
| // Don't fetch or cache the image if the display of this element is turned |
| // off. |
| if (computed_style()->display() != cssom::KeywordValue::GetNone()) { |
| scoped_refptr<cssom::PropertyValue> background_image = |
| computed_style()->background_image(); |
| |
| cssom::PropertyListValue* property_list_value = |
| base::polymorphic_downcast<cssom::PropertyListValue*>( |
| background_image.get()); |
| |
| loader::image::CachedImageReferenceVector cached_images; |
| for (size_t i = 0; i < property_list_value->value().size(); ++i) { |
| // Skip this image if it is not an absolute URL. |
| if (property_list_value->value()[i]->GetTypeId() != |
| base::GetTypeId<cssom::AbsoluteURLValue>()) { |
| continue; |
| } |
| |
| // Skip invalid URL. |
| GURL absolute_url = base::polymorphic_downcast<cssom::AbsoluteURLValue*>( |
| property_list_value->value()[i].get()) |
| ->value(); |
| if (!absolute_url.is_valid()) { |
| continue; |
| } |
| |
| active_background_images_.push_back(absolute_url); |
| html_element_context()->animated_image_tracker()->IncreaseURLCount( |
| absolute_url); |
| |
| scoped_refptr<loader::image::CachedImage> cached_image = |
| html_element_context()->image_cache()->GetOrCreateCachedResource( |
| absolute_url, loader::Origin()); |
| base::Closure loaded_callback = base::Bind( |
| &HTMLElement::OnBackgroundImageLoaded, base::Unretained(this)); |
| cached_images.emplace_back( |
| new loader::image::CachedImageReferenceWithCallbacks( |
| cached_image, loaded_callback, base::Closure())); |
| } |
| |
| ClearCachedBackgroundImages(); |
| cached_background_images_ = std::move(cached_images); |
| } else { |
| // Clear the previous cached background image if the display is "none". |
| ClearCachedBackgroundImages(); |
| } |
| } |
| |
| void HTMLElement::GetLoadTimingInfoAndCreateResourceTiming() { |
| if (html_element_context()->performance() == nullptr) return; |
| for (auto& cached_background_image : cached_background_images_) { |
| scoped_refptr<loader::CachedResourceBase> cached_image = |
| cached_background_image->GetCachedResource(); |
| if (cached_image == nullptr) continue; |
| |
| if (!cached_image->get_resource_timing_created_flag()) { |
| html_element_context()->performance()->CreatePerformanceResourceTiming( |
| cached_image->GetLoadTimingInfo(), |
| kPerformanceResourceTimingInitiatorType, cached_image->url().spec()); |
| cached_image->set_resource_timing_created_flag(true); |
| } |
| } |
| } |
| |
| void HTMLElement::OnBackgroundImageLoaded() { |
| node_document()->RecordMutation(); |
| InvalidateLayoutBoxRenderTreeNodes(); |
| // GetLoadTimingInfo from cached resource and create resource timing. |
| GetLoadTimingInfoAndCreateResourceTiming(); |
| } |
| |
| } // namespace dom |
| } // namespace cobalt |