blob: ef21c629d0748b05a43b98cf5de1b07895f6cb30 [file] [log] [blame]
// 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