/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/Preferences.h" #include "mozilla/dom/ShadowRoot.h" #include "mozilla/dom/DocumentFragment.h" #include "ChildIterator.h" #include "nsContentUtils.h" #include "nsIStyleSheetLinkingElement.h" #include "nsWindowSizes.h" #include "nsXBLPrototypeBinding.h" #include "mozilla/dom/DirectionalityUtils.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLSlotElement.h" #include "mozilla/EventDispatcher.h" #include "mozilla/ServoStyleRuleMap.h" #include "mozilla/StyleSheet.h" #include "mozilla/StyleSheetInlines.h" #include "mozilla/dom/StyleSheetList.h" using namespace mozilla; using namespace mozilla::dom; NS_IMPL_CYCLE_COLLECTION_CLASS(ShadowRoot) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ShadowRoot, DocumentFragment) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets) for (StyleSheet* sheet : tmp->mStyleSheets) { // mServoStyles keeps another reference to it if applicable. if (sheet->IsApplicable()) { MOZ_ASSERT(tmp->mServoStyles); NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mServoStyles->sheets[i]"); cb.NoteXPCOMChild(sheet); } } NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStyleSheets) for (auto iter = tmp->mIdentifierMap.ConstIter(); !iter.Done(); iter.Next()) { iter.Get()->Traverse(&cb); } DocumentOrShadowRoot::Traverse(tmp, cb); NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ShadowRoot) if (tmp->GetHost()) { tmp->GetHost()->RemoveMutationObserver(tmp); } NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMStyleSheets) tmp->mIdentifierMap.Clear(); DocumentOrShadowRoot::Unlink(tmp); NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DocumentFragment) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ShadowRoot) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContent) NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) NS_INTERFACE_MAP_ENTRY(nsIRadioGroupContainer) NS_INTERFACE_MAP_END_INHERITING(DocumentFragment) NS_IMPL_ADDREF_INHERITED(ShadowRoot, DocumentFragment) NS_IMPL_RELEASE_INHERITED(ShadowRoot, DocumentFragment) ShadowRoot::ShadowRoot(Element* aElement, ShadowRootMode aMode, already_AddRefed&& aNodeInfo) : DocumentFragment(std::move(aNodeInfo)), DocumentOrShadowRoot(*this), mMode(aMode), mIsUAWidget(false) { SetHost(aElement); // Nodes in a shadow tree should never store a value // in the subtree root pointer, nodes in the shadow tree // track the subtree root using GetContainingShadow(). ClearSubtreeRootPointer(); SetFlags(NODE_IS_IN_SHADOW_TREE); Bind(); ExtendedDOMSlots()->mBindingParent = aElement; ExtendedDOMSlots()->mContainingShadow = this; // Add the ShadowRoot as a mutation observer on the host to watch // for mutations because the insertion points in this ShadowRoot // may need to be updated when the host children are modified. GetHost()->AddMutationObserver(this); } ShadowRoot::~ShadowRoot() { if (auto* host = GetHost()) { // mHost may have been unlinked. host->RemoveMutationObserver(this); } if (IsInComposedDoc()) { OwnerDoc()->RemoveComposedDocShadowRoot(*this); } MOZ_DIAGNOSTIC_ASSERT(!OwnerDoc()->IsComposedDocShadowRoot(*this)); UnsetFlags(NODE_IS_IN_SHADOW_TREE); // nsINode destructor expects mSubtreeRoot == this. SetSubtreeRootPointer(this); } MOZ_DEFINE_MALLOC_SIZE_OF(ShadowRootAuthorStylesMallocSizeOf) MOZ_DEFINE_MALLOC_ENCLOSING_SIZE_OF(ShadowRootAuthorStylesMallocEnclosingSizeOf) void ShadowRoot::AddSizeOfExcludingThis(nsWindowSizes& aSizes, size_t* aNodeSize) const { DocumentFragment::AddSizeOfExcludingThis(aSizes, aNodeSize); DocumentOrShadowRoot::AddSizeOfExcludingThis(aSizes); aSizes.mLayoutShadowDomAuthorStyles += Servo_AuthorStyles_SizeOfIncludingThis( ShadowRootAuthorStylesMallocSizeOf, ShadowRootAuthorStylesMallocEnclosingSizeOf, mServoStyles.get()); } JSObject* ShadowRoot::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return mozilla::dom::ShadowRoot_Binding::Wrap(aCx, this, aGivenProto); } void ShadowRoot::CloneInternalDataFrom(ShadowRoot* aOther) { size_t sheetCount = aOther->SheetCount(); for (size_t i = 0; i < sheetCount; ++i) { StyleSheet* sheet = aOther->SheetAt(i); if (sheet->IsApplicable()) { RefPtr clonedSheet = sheet->Clone(nullptr, nullptr, this, nullptr); if (clonedSheet) { AppendStyleSheet(*clonedSheet.get()); } } } } nsresult ShadowRoot::Bind() { MOZ_ASSERT(!IsInComposedDoc(), "Forgot to unbind?"); if (Host()->IsInComposedDoc()) { SetIsConnected(true); OwnerDoc()->AddComposedDocShadowRoot(*this); } for (nsIContent* child = GetFirstChild(); child; child = child->GetNextSibling()) { nsresult rv = child->BindToTree(nullptr, this, Host()); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } void ShadowRoot::Unbind() { if (IsInComposedDoc()) { SetIsConnected(false); OwnerDoc()->RemoveComposedDocShadowRoot(*this); } for (nsIContent* child = GetFirstChild(); child; child = child->GetNextSibling()) { child->UnbindFromTree(true, false); } } void ShadowRoot::Unattach() { MOZ_ASSERT(!HasSlots(), "Won't work!"); if (!GetHost()) { // It is possible that we've been unlinked already. In such case host // should have called Unbind and ShadowRoot's own unlink // RemoveMutationObserver. return; } Unbind(); GetHost()->RemoveMutationObserver(this); SetHost(nullptr); } void ShadowRoot::InvalidateStyleAndLayoutOnSubtree(Element* aElement) { MOZ_ASSERT(aElement); nsIDocument* doc = GetComposedDoc(); if (!doc) { return; } nsIPresShell* shell = doc->GetShell(); if (!shell) { return; } shell->DestroyFramesForAndRestyle(aElement); } void ShadowRoot::AddSlot(HTMLSlotElement* aSlot) { MOZ_ASSERT(aSlot); // Note that if name attribute missing, the slot is a default slot. nsAutoString name; aSlot->GetName(name); nsTArray* currentSlots = mSlotMap.LookupOrAdd(name); MOZ_ASSERT(currentSlots); HTMLSlotElement* oldSlot = currentSlots->SafeElementAt(0); TreeOrderComparator comparator; currentSlots->InsertElementSorted(aSlot, comparator); HTMLSlotElement* currentSlot = currentSlots->ElementAt(0); if (currentSlot != aSlot) { return; } if (oldSlot && oldSlot != currentSlot) { // Move assigned nodes from old slot to new slot. InvalidateStyleAndLayoutOnSubtree(oldSlot); const nsTArray>& assignedNodes = oldSlot->AssignedNodes(); bool doEnqueueSlotChange = false; while (assignedNodes.Length() > 0) { nsINode* assignedNode = assignedNodes[0]; oldSlot->RemoveAssignedNode(assignedNode); currentSlot->AppendAssignedNode(assignedNode); doEnqueueSlotChange = true; } if (doEnqueueSlotChange) { oldSlot->EnqueueSlotChangeEvent(); currentSlot->EnqueueSlotChangeEvent(); SlotStateChanged(oldSlot); SlotStateChanged(currentSlot); } } else { bool doEnqueueSlotChange = false; // Otherwise add appropriate nodes to this slot from the host. for (nsIContent* child = GetHost()->GetFirstChild(); child; child = child->GetNextSibling()) { nsAutoString slotName; if (child->IsElement()) { child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::slot, slotName); } if (!child->IsSlotable() || !slotName.Equals(name)) { continue; } doEnqueueSlotChange = true; currentSlot->AppendAssignedNode(child); } if (doEnqueueSlotChange) { currentSlot->EnqueueSlotChangeEvent(); SlotStateChanged(currentSlot); } } } void ShadowRoot::RemoveSlot(HTMLSlotElement* aSlot) { MOZ_ASSERT(aSlot); nsAutoString name; aSlot->GetName(name); SlotArray* currentSlots = mSlotMap.Get(name); MOZ_DIAGNOSTIC_ASSERT(currentSlots && currentSlots->Contains(aSlot), "Slot to deregister wasn't found?"); if (currentSlots->Length() == 1) { MOZ_ASSERT(currentSlots->ElementAt(0) == aSlot); InvalidateStyleAndLayoutOnSubtree(aSlot); mSlotMap.Remove(name); if (!aSlot->AssignedNodes().IsEmpty()) { aSlot->ClearAssignedNodes(); aSlot->EnqueueSlotChangeEvent(); } return; } const bool wasFirstSlot = currentSlots->ElementAt(0) == aSlot; currentSlots->RemoveElement(aSlot); // Move assigned nodes from removed slot to the next slot in // tree order with the same name. if (!wasFirstSlot) { return; } InvalidateStyleAndLayoutOnSubtree(aSlot); HTMLSlotElement* replacementSlot = currentSlots->ElementAt(0); const nsTArray>& assignedNodes = aSlot->AssignedNodes(); bool slottedNodesChanged = !assignedNodes.IsEmpty(); while (!assignedNodes.IsEmpty()) { nsINode* assignedNode = assignedNodes[0]; aSlot->RemoveAssignedNode(assignedNode); replacementSlot->AppendAssignedNode(assignedNode); } if (slottedNodesChanged) { aSlot->EnqueueSlotChangeEvent(); replacementSlot->EnqueueSlotChangeEvent(); } } // FIXME(emilio): There's a bit of code duplication between this and the // equivalent ServoStyleSet methods, it'd be nice to not duplicate it... void ShadowRoot::RuleAdded(StyleSheet& aSheet, css::Rule& aRule) { if (!aSheet.IsApplicable()) { return; } MOZ_ASSERT(mServoStyles); if (mStyleRuleMap) { mStyleRuleMap->RuleAdded(aSheet, aRule); } Servo_AuthorStyles_ForceDirty(mServoStyles.get()); ApplicableRulesChanged(); } void ShadowRoot::RuleRemoved(StyleSheet& aSheet, css::Rule& aRule) { if (!aSheet.IsApplicable()) { return; } MOZ_ASSERT(mServoStyles); if (mStyleRuleMap) { mStyleRuleMap->RuleRemoved(aSheet, aRule); } Servo_AuthorStyles_ForceDirty(mServoStyles.get()); ApplicableRulesChanged(); } void ShadowRoot::RuleChanged(StyleSheet& aSheet, css::Rule*) { if (!aSheet.IsApplicable()) { return; } MOZ_ASSERT(mServoStyles); Servo_AuthorStyles_ForceDirty(mServoStyles.get()); ApplicableRulesChanged(); } void ShadowRoot::ApplicableRulesChanged() { nsIDocument* doc = GetComposedDoc(); if (!doc) { return; } if (nsIPresShell* shell = doc->GetShell()) { shell->RecordShadowStyleChange(*this); } } void ShadowRoot::InsertSheetAt(size_t aIndex, StyleSheet& aSheet) { DocumentOrShadowRoot::InsertSheetAt(aIndex, aSheet); if (aSheet.IsApplicable()) { InsertSheetIntoAuthorData(aIndex, aSheet); } } void ShadowRoot::InsertSheetIntoAuthorData(size_t aIndex, StyleSheet& aSheet) { MOZ_ASSERT(SheetAt(aIndex) == &aSheet); MOZ_ASSERT(aSheet.IsApplicable()); if (!mServoStyles) { mServoStyles.reset(Servo_AuthorStyles_Create()); } if (mStyleRuleMap) { mStyleRuleMap->SheetAdded(aSheet); } for (size_t i = aIndex + 1; i < SheetCount(); ++i) { StyleSheet* beforeSheet = SheetAt(i); if (!beforeSheet->IsApplicable()) { continue; } Servo_AuthorStyles_InsertStyleSheetBefore(mServoStyles.get(), &aSheet, beforeSheet); ApplicableRulesChanged(); return; } Servo_AuthorStyles_AppendStyleSheet(mServoStyles.get(), &aSheet); ApplicableRulesChanged(); } // FIXME(emilio): This needs to notify document observers and such, // presumably. void ShadowRoot::StyleSheetApplicableStateChanged(StyleSheet& aSheet, bool aApplicable) { int32_t index = IndexOfSheet(aSheet); if (index < 0) { // NOTE(emilio): @import sheets are handled in the relevant RuleAdded // notification, which only notifies after the sheet is loaded. // // This setup causes weirdness in other places, we may want to fix this in // bug 1465031. MOZ_DIAGNOSTIC_ASSERT(aSheet.GetParentSheet(), "It'd better be an @import sheet"); return; } if (aApplicable) { InsertSheetIntoAuthorData(size_t(index), aSheet); } else { MOZ_ASSERT(mServoStyles); if (mStyleRuleMap) { mStyleRuleMap->SheetRemoved(aSheet); } Servo_AuthorStyles_RemoveStyleSheet(mServoStyles.get(), &aSheet); ApplicableRulesChanged(); } } void ShadowRoot::RemoveSheet(StyleSheet* aSheet) { MOZ_ASSERT(aSheet); RefPtr sheet = DocumentOrShadowRoot::RemoveSheet(*aSheet); MOZ_ASSERT(sheet); if (sheet->IsApplicable()) { MOZ_ASSERT(mServoStyles); if (mStyleRuleMap) { mStyleRuleMap->SheetRemoved(*sheet); } Servo_AuthorStyles_RemoveStyleSheet(mServoStyles.get(), sheet); ApplicableRulesChanged(); } } void ShadowRoot::AddToIdTable(Element* aElement, nsAtom* aId) { nsIdentifierMapEntry* entry = mIdentifierMap.PutEntry(aId); if (entry) { entry->AddIdElement(aElement); } } void ShadowRoot::RemoveFromIdTable(Element* aElement, nsAtom* aId) { nsIdentifierMapEntry* entry = mIdentifierMap.GetEntry(aId); if (entry) { entry->RemoveIdElement(aElement); if (entry->IsEmpty()) { mIdentifierMap.RemoveEntry(entry); } } } void ShadowRoot::GetEventTargetParent(EventChainPreVisitor& aVisitor) { aVisitor.mCanHandle = true; aVisitor.mRootOfClosedTree = IsClosed(); // Inform that we're about to exit the current scope. aVisitor.mRelatedTargetRetargetedInCurrentScope = false; // https://dom.spec.whatwg.org/#ref-for-get-the-parent%E2%91%A6 if (!aVisitor.mEvent->mFlags.mComposed) { nsCOMPtr originalTarget = do_QueryInterface(aVisitor.mEvent->mOriginalTarget); if (originalTarget->GetContainingShadow() == this) { // If we do stop propagation, we still want to propagate // the event to chrome (nsPIDOMWindow::GetParentTarget()). // The load event is special in that we don't ever propagate it // to chrome. nsCOMPtr win = OwnerDoc()->GetWindow(); EventTarget* parentTarget = win && aVisitor.mEvent->mMessage != eLoad ? win->GetParentTarget() : nullptr; aVisitor.SetParentTarget(parentTarget, true); return; } } nsIContent* shadowHost = GetHost(); aVisitor.SetParentTarget(shadowHost, false); nsCOMPtr content(do_QueryInterface(aVisitor.mEvent->mTarget)); if (content && content->GetBindingParent() == shadowHost) { aVisitor.mEventTargetAtParent = shadowHost; } } ShadowRoot::SlotAssignment ShadowRoot::SlotAssignmentFor(nsIContent* aContent) { nsAutoString slotName; // Note that if slot attribute is missing, assign it to the first default // slot, if exists. if (aContent->IsElement()) { aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::slot, slotName); } nsTArray* slots = mSlotMap.Get(slotName); if (!slots) { return {}; } HTMLSlotElement* slot = slots->ElementAt(0); MOZ_ASSERT(slot); // Find the appropriate position in the assigned node list for the // newly assigned content. const nsTArray>& assignedNodes = slot->AssignedNodes(); nsIContent* currentContent = GetHost()->GetFirstChild(); Maybe insertionIndex; for (uint32_t i = 0; i < assignedNodes.Length(); i++) { // Seek through the host's explicit children until the // assigned content is found. while (currentContent && currentContent != assignedNodes[i]) { if (currentContent == aContent) { insertionIndex.emplace(i); break; } currentContent = currentContent->GetNextSibling(); } if (insertionIndex) { break; } } return {slot, insertionIndex}; } void ShadowRoot::MaybeReassignElement(Element* aElement) { MOZ_ASSERT(aElement->GetParent() == GetHost()); HTMLSlotElement* oldSlot = aElement->GetAssignedSlot(); SlotAssignment assignment = SlotAssignmentFor(aElement); if (assignment.mSlot == oldSlot) { // Nothing to do here. return; } if (nsIDocument* doc = GetComposedDoc()) { if (nsIPresShell* shell = doc->GetShell()) { shell->SlotAssignmentWillChange(*aElement, oldSlot, assignment.mSlot); } } if (oldSlot) { oldSlot->RemoveAssignedNode(aElement); oldSlot->EnqueueSlotChangeEvent(); } if (assignment.mSlot) { if (assignment.mIndex) { assignment.mSlot->InsertAssignedNode(*assignment.mIndex, aElement); } else { assignment.mSlot->AppendAssignedNode(aElement); } assignment.mSlot->EnqueueSlotChangeEvent(); } SlotStateChanged(oldSlot); SlotStateChanged(assignment.mSlot); } Element* ShadowRoot::GetActiveElement() { return GetRetargetedFocusedElement(); } void ShadowRoot::GetInnerHTML(nsAString& aInnerHTML) { GetMarkup(false, aInnerHTML); } void ShadowRoot::SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError) { SetInnerHTMLInternal(aInnerHTML, aError); } nsINode* ShadowRoot::ImportNodeAndAppendChildAt(nsINode& aParentNode, nsINode& aNode, bool aDeep, mozilla::ErrorResult& rv) { MOZ_ASSERT(mIsUAWidget); if (!aParentNode.IsInUAWidget()) { rv.Throw(NS_ERROR_INVALID_ARG); return nullptr; } RefPtr node = OwnerDoc()->ImportNode(aNode, aDeep, rv); if (rv.Failed()) { return nullptr; } return aParentNode.AppendChild(*node, rv); } nsINode* ShadowRoot::CreateElementAndAppendChildAt(nsINode& aParentNode, const nsAString& aTagName, mozilla::ErrorResult& rv) { MOZ_ASSERT(mIsUAWidget); MOZ_ASSERT(OwnerDoc()); if (!aParentNode.IsInUAWidget()) { rv.Throw(NS_ERROR_INVALID_ARG); return nullptr; } // This option is not exposed to UA Widgets ElementCreationOptionsOrString options; RefPtr node = OwnerDoc()->CreateElement(aTagName, options, rv); if (rv.Failed()) { return nullptr; } return aParentNode.AppendChild(*node, rv); } void ShadowRoot::AttributeChanged(Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, const nsAttrValue* aOldValue) { if (aNameSpaceID != kNameSpaceID_None || aAttribute != nsGkAtoms::slot) { return; } if (aElement->GetParent() != GetHost()) { return; } MaybeReassignElement(aElement); } void ShadowRoot::ContentAppended(nsIContent* aFirstNewContent) { for (nsIContent* content = aFirstNewContent; content; content = content->GetNextSibling()) { ContentInserted(content); } } void ShadowRoot::ContentInserted(nsIContent* aChild) { // Check to ensure that the child not an anonymous subtree root because // even though its parent could be the host it may not be in the host's child // list. if (aChild->IsRootOfAnonymousSubtree()) { return; } if (!aChild->IsSlotable()) { return; } if (aChild->GetParent() == GetHost()) { SlotAssignment assignment = SlotAssignmentFor(aChild); if (!assignment.mSlot) { return; } // Fallback content will go away, let layout know. if (assignment.mSlot->AssignedNodes().IsEmpty()) { InvalidateStyleAndLayoutOnSubtree(assignment.mSlot); } if (assignment.mIndex) { assignment.mSlot->InsertAssignedNode(*assignment.mIndex, aChild); } else { assignment.mSlot->AppendAssignedNode(aChild); } assignment.mSlot->EnqueueSlotChangeEvent(); SlotStateChanged(assignment.mSlot); return; } // If parent's root is a shadow root, and parent is a slot whose assigned // nodes is the empty list, then run signal a slot change for parent. HTMLSlotElement* slot = HTMLSlotElement::FromNodeOrNull(aChild->GetParent()); if (slot && slot->GetContainingShadow() == this && slot->AssignedNodes().IsEmpty()) { slot->EnqueueSlotChangeEvent(); } } void ShadowRoot::ContentRemoved(nsIContent* aChild, nsIContent* aPreviousSibling) { // Check to ensure that the child not an anonymous subtree root because // even though its parent could be the host it may not be in the host's child // list. if (aChild->IsRootOfAnonymousSubtree()) { return; } if (!aChild->IsSlotable()) { return; } if (aChild->GetParent() == GetHost()) { if (HTMLSlotElement* slot = aChild->GetAssignedSlot()) { // If the slot is going to start showing fallback content, we need to tell // layout about it. if (slot->AssignedNodes().Length() == 1) { InvalidateStyleAndLayoutOnSubtree(slot); } slot->RemoveAssignedNode(aChild); slot->EnqueueSlotChangeEvent(); } return; } // If parent's root is a shadow root, and parent is a slot whose assigned // nodes is the empty list, then run signal a slot change for parent. HTMLSlotElement* slot = HTMLSlotElement::FromNodeOrNull(aChild->GetParent()); if (slot && slot->GetContainingShadow() == this && slot->AssignedNodes().IsEmpty()) { slot->EnqueueSlotChangeEvent(); } } ServoStyleRuleMap& ShadowRoot::ServoStyleRuleMap() { if (!mStyleRuleMap) { mStyleRuleMap = MakeUnique(); } mStyleRuleMap->EnsureTable(*this); return *mStyleRuleMap; } nsresult ShadowRoot::Clone(dom::NodeInfo* aNodeInfo, nsINode** aResult) const { *aResult = nullptr; return NS_ERROR_DOM_NOT_SUPPORTED_ERR; }