blob: 5a0d0c8b999698715bab81b3f7718bfd7bd8b23d [file] [log] [blame]
/*
* Copyright (C) 2010, 2011 Nokia Corporation and/or its subsidiary(-ies)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "third_party/blink/renderer/core/html/html_details_element.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/strings/grit/blink_strings.h"
#include "third_party/blink/renderer/core/css/css_property_names.h"
#include "third_party/blink/renderer/core/css_value_keywords.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/events/mutation_event_suppression_scope.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/slot_assignment.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html/html_summary_element.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_ng_block_flow.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
namespace blink {
HTMLDetailsElement::HTMLDetailsElement(Document& document)
: HTMLElement(html_names::kDetailsTag, document) {
UseCounter::Count(document, WebFeature::kDetailsElement);
EnsureUserAgentShadowRoot(SlotAssignmentMode::kManual);
}
HTMLDetailsElement::~HTMLDetailsElement() = default;
void HTMLDetailsElement::DispatchPendingEvent(
const AttributeModificationReason reason) {
Event* toggle_event = nullptr;
CHECK(pending_toggle_event_);
toggle_event = pending_toggle_event_.Get();
pending_toggle_event_ = nullptr;
if (reason == AttributeModificationReason::kByParser)
GetDocument().SetToggleDuringParsing(true);
DispatchEvent(*toggle_event);
if (reason == AttributeModificationReason::kByParser)
GetDocument().SetToggleDuringParsing(false);
}
LayoutObject* HTMLDetailsElement::CreateLayoutObject(
const ComputedStyle& style) {
if (RuntimeEnabledFeatures::DetailsStylingEnabled()) {
return HTMLElement::CreateLayoutObject(style);
}
return LayoutObject::CreateBlockFlowOrListItem(this, style);
}
// Creates shadow DOM:
// #shadowroot
// <SLOT id="details-summary">
// <SUMMARY>Details</SUMMARY>
// </SLOT>
// <SLOT id="details-content"
// style="content-visibility: hidden; display:block;"></SLOT>
// <STYLE>...
void HTMLDetailsElement::DidAddUserAgentShadowRoot(ShadowRoot& root) {
auto* default_summary =
MakeGarbageCollected<HTMLSummaryElement>(GetDocument());
default_summary->AppendChild(
Text::Create(GetDocument(),
GetLocale().QueryString(IDS_DETAILS_WITHOUT_SUMMARY_LABEL)));
summary_slot_ = MakeGarbageCollected<HTMLSlotElement>(GetDocument());
summary_slot_->SetIdAttribute(shadow_element_names::kIdDetailsSummary);
summary_slot_->AppendChild(default_summary);
root.AppendChild(summary_slot_);
content_slot_ = MakeGarbageCollected<HTMLSlotElement>(GetDocument());
content_slot_->SetIdAttribute(shadow_element_names::kIdDetailsContent);
if (RuntimeEnabledFeatures::DetailsStylingEnabled()) {
content_slot_->SetShadowPseudoId(shadow_element_names::kIdDetailsContent);
}
content_slot_->SetInlineStyleProperty(CSSPropertyID::kContentVisibility,
CSSValueID::kHidden);
content_slot_->EnsureDisplayLockContext().SetIsDetailsSlotElement(true);
content_slot_->SetInlineStyleProperty(CSSPropertyID::kDisplay,
CSSValueID::kBlock);
root.AppendChild(content_slot_);
auto* default_summary_style =
MakeGarbageCollected<HTMLStyleElement>(GetDocument());
// This style is required only if this <details> shows the UA-provided
// <summary>, not a light child <summary>.
default_summary_style->setTextContent(R"CSS(
:host summary {
display: list-item;
counter-increment: list-item 0;
list-style: disclosure-closed inside;
}
:host([open]) summary {
list-style-type: disclosure-open;
}
)CSS");
root.AppendChild(default_summary_style);
}
Element* HTMLDetailsElement::FindMainSummary() const {
if (HTMLSummaryElement* summary =
Traversal<HTMLSummaryElement>::FirstChild(*this))
return summary;
HTMLSlotElement& slot =
To<HTMLSlotElement>(*UserAgentShadowRoot()->firstChild());
CHECK(IsA<HTMLSummaryElement>(*slot.firstChild()));
return To<Element>(slot.firstChild());
}
void HTMLDetailsElement::ManuallyAssignSlots() {
HeapVector<Member<Node>> summary_nodes;
HeapVector<Member<Node>> content_nodes;
for (Node& child : NodeTraversal::ChildrenOf(*this)) {
if (!child.IsSlotable()) {
CHECK(!IsA<HTMLSummaryElement>(child));
continue;
}
bool is_first_summary =
summary_nodes.empty() && IsA<HTMLSummaryElement>(child);
if (is_first_summary) {
summary_nodes.push_back(child);
} else {
content_nodes.push_back(child);
}
}
summary_slot_->Assign(summary_nodes);
content_slot_->Assign(content_nodes);
}
void HTMLDetailsElement::Trace(Visitor* visitor) const {
visitor->Trace(pending_toggle_event_);
visitor->Trace(summary_slot_);
visitor->Trace(content_slot_);
HTMLElement::Trace(visitor);
}
void HTMLDetailsElement::ParseAttribute(
const AttributeModificationParams& params) {
CHECK(params.reason != AttributeModificationReason::kByParser ||
!parentNode())
<< "This code depends on the parser setting attributes before inserting "
"into the document; if that were not the case we would need to "
"handle setting of either open or name by the parser, and "
"potentially change the open attribute during that handling.";
if (params.name == html_names::kOpenAttr) {
bool old_value = is_open_;
is_open_ = !params.new_value.IsNull();
if (is_open_ == old_value)
return;
// Dispatch toggle event asynchronously.
String old_state = is_open_ ? "closed" : "open";
String new_state = is_open_ ? "open" : "closed";
if (pending_toggle_event_) {
old_state = pending_toggle_event_->oldState();
}
pending_toggle_event_ =
ToggleEvent::Create(event_type_names::kToggle, Event::Cancelable::kNo,
old_state, new_state);
pending_event_task_ = PostCancellableTask(
*GetDocument().GetTaskRunner(TaskType::kDOMManipulation), FROM_HERE,
WTF::BindOnce(&HTMLDetailsElement::DispatchPendingEvent,
WrapPersistent(this), params.reason));
Element* content =
EnsureUserAgentShadowRoot(SlotAssignmentMode::kManual)
.getElementById(shadow_element_names::kIdDetailsContent);
DCHECK(content);
if (is_open_) {
content->RemoveInlineStyleProperty(CSSPropertyID::kContentVisibility);
content->RemoveInlineStyleProperty(CSSPropertyID::kDisplay);
// https://html.spec.whatwg.org/multipage/interactive-elements.html#ensure-details-exclusivity-by-closing-other-elements-if-needed
//
// The name attribute links multiple details elements into an
// exclusive accordion. So if this one has a name, close the
// other ones with the same name.
CHECK_NE(params.reason,
AttributeModificationReason::kBySynchronizationOfLazyAttribute);
// TODO(https://crbug.com/1444057): Should this be in
// AttributeChanged instead?
if (!GetName().empty() &&
params.reason == AttributeModificationReason::kDirectly) {
// Don't fire mutation events for any changes to the open attribute
// that this causes.
MutationEventSuppressionScope scope(GetDocument());
HeapVector<Member<HTMLDetailsElement>> details_with_name(
OtherElementsInNameGroup());
for (HTMLDetailsElement* other_details : details_with_name) {
CHECK_NE(other_details, this);
UseCounter::Count(
GetDocument(),
WebFeature::kHTMLDetailsElementNameAttributeClosesOther);
other_details->setAttribute(html_names::kOpenAttr, g_null_atom);
}
}
} else {
content->SetInlineStyleProperty(CSSPropertyID::kDisplay,
CSSValueID::kBlock);
content->SetInlineStyleProperty(CSSPropertyID::kContentVisibility,
CSSValueID::kHidden);
content->EnsureDisplayLockContext().SetIsDetailsSlotElement(true);
}
} else {
HTMLElement::ParseAttribute(params);
}
}
void HTMLDetailsElement::AttributeChanged(
const AttributeModificationParams& params) {
const QualifiedName& name = params.name;
if (name == html_names::kNameAttr) {
if (!params.new_value.empty()) {
UseCounter::Count(GetDocument(),
WebFeature::kHTMLDetailsElementNameAttribute);
}
MaybeCloseForExclusivity();
}
HTMLElement::AttributeChanged(params);
}
Node::InsertionNotificationRequest HTMLDetailsElement::InsertedInto(
ContainerNode& insertion_point) {
Node::InsertionNotificationRequest result =
HTMLElement::InsertedInto(insertion_point);
MaybeCloseForExclusivity();
return result;
}
// https://html.spec.whatwg.org/multipage/C#ensure-details-exclusivity-by-closing-the-given-element-if-needed
void HTMLDetailsElement::MaybeCloseForExclusivity() {
if (GetName().empty() || !is_open_) {
return;
}
// Don't fire mutation events for any changes to the open attribute
// that this causes.
MutationEventSuppressionScope scope(GetDocument());
HeapVector<Member<HTMLDetailsElement>> details_with_name(
OtherElementsInNameGroup());
for (HTMLDetailsElement* other_details : details_with_name) {
CHECK_NE(other_details, this);
if (other_details->is_open_) {
// close this details element
UseCounter::Count(GetDocument(),
WebFeature::kHTMLDetailsElementNameAttributeClosesSelf);
ToggleOpen();
break;
}
}
}
void HTMLDetailsElement::ToggleOpen() {
setAttribute(html_names::kOpenAttr, is_open_ ? g_null_atom : g_empty_atom);
}
HeapVector<Member<HTMLDetailsElement>>
HTMLDetailsElement::OtherElementsInNameGroup() {
HeapVector<Member<HTMLDetailsElement>> result;
const AtomicString& name = GetName();
if (name.empty()) {
return result;
}
HTMLDetailsElement* details = Traversal<HTMLDetailsElement>::Next(TreeRoot());
while (details) {
if (details != this && details->GetName() == name) {
result.push_back(details);
}
details = Traversal<HTMLDetailsElement>::Next(*details);
}
return result;
}
bool HTMLDetailsElement::IsInteractiveContent() const {
return true;
}
// static
bool HTMLDetailsElement::ExpandDetailsAncestors(const Node& node) {
CHECK(&node);
// Since setting the open attribute fires mutation events which could mess
// with the FlatTreeTraversal iterator, we should first iterate details
// elements to open and then open them all.
HeapVector<Member<HTMLDetailsElement>> details_to_open;
for (Node& parent : FlatTreeTraversal::AncestorsOf(node)) {
if (HTMLDetailsElement* details = DynamicTo<HTMLDetailsElement>(parent)) {
// If the active match is inside the <summary> of a <details>, then we
// shouldn't expand the <details> because the active match is already
// visible.
bool inside_summary = false;
Element& summary = *details->FindMainSummary();
for (Node& ancestor : FlatTreeTraversal::AncestorsOf(node)) {
if (&ancestor == &summary) {
inside_summary = true;
break;
}
}
if (!inside_summary &&
!details->FastHasAttribute(html_names::kOpenAttr)) {
details_to_open.push_back(details);
}
}
}
for (HTMLDetailsElement* details : details_to_open) {
details->setAttribute(html_names::kOpenAttr, g_empty_atom);
}
return details_to_open.size();
}
bool HTMLDetailsElement::IsValidInvokeAction(HTMLElement& invoker,
InvokeAction action) {
bool parent_is_valid = HTMLElement::IsValidInvokeAction(invoker, action);
if (!RuntimeEnabledFeatures::HTMLInvokeActionsV2Enabled()) {
return parent_is_valid;
}
return parent_is_valid || action == InvokeAction::kToggle ||
action == InvokeAction::kOpen || action == InvokeAction::kClose;
}
bool HTMLDetailsElement::HandleInvokeInternal(HTMLElement& invoker,
InvokeAction action) {
CHECK(IsValidInvokeAction(invoker, action));
if (HTMLElement::HandleInvokeInternal(invoker, action)) {
return true;
}
if (action == InvokeAction::kAuto || action == InvokeAction::kToggle) {
ToggleOpen();
return true;
} else if (action == InvokeAction::kClose) {
if (is_open_) {
setAttribute(html_names::kOpenAttr, g_null_atom);
}
return true;
} else if (action == InvokeAction::kOpen) {
if (!is_open_) {
setAttribute(html_names::kOpenAttr, g_empty_atom);
}
return true;
}
return false;
}
} // namespace blink