HTMLSlotElement.cpp (13167B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/HTMLSlotElement.h" 8 9 #include "mozilla/AppShutdown.h" 10 #include "mozilla/PresShell.h" 11 #include "mozilla/dom/DocGroup.h" 12 #include "mozilla/dom/Document.h" 13 #include "mozilla/dom/HTMLSlotElementBinding.h" 14 #include "mozilla/dom/HTMLUnknownElement.h" 15 #include "mozilla/dom/ShadowRoot.h" 16 #include "mozilla/dom/Text.h" 17 #include "nsContentUtils.h" 18 #include "nsGkAtoms.h" 19 20 nsGenericHTMLElement* NS_NewHTMLSlotElement( 21 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, 22 mozilla::dom::FromParser aFromParser) { 23 RefPtr<mozilla::dom::NodeInfo> nodeInfo(std::move(aNodeInfo)); 24 auto* nim = nodeInfo->NodeInfoManager(); 25 return new (nim) mozilla::dom::HTMLSlotElement(nodeInfo.forget()); 26 } 27 28 namespace mozilla::dom { 29 30 HTMLSlotElement::HTMLSlotElement( 31 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) 32 : nsGenericHTMLElement(std::move(aNodeInfo)) {} 33 34 HTMLSlotElement::~HTMLSlotElement() { 35 for (const auto& node : mManuallyAssignedNodes) { 36 MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this); 37 node->AsContent()->SetManualSlotAssignment(nullptr); 38 } 39 } 40 41 NS_IMPL_ADDREF_INHERITED(HTMLSlotElement, nsGenericHTMLElement) 42 NS_IMPL_RELEASE_INHERITED(HTMLSlotElement, nsGenericHTMLElement) 43 44 NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLSlotElement, nsGenericHTMLElement, 45 mAssignedNodes) 46 47 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLSlotElement) 48 NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElement) 49 50 NS_IMPL_ELEMENT_CLONE(HTMLSlotElement) 51 52 nsresult HTMLSlotElement::BindToTree(BindContext& aContext, nsINode& aParent) { 53 RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow(); 54 55 nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); 56 NS_ENSURE_SUCCESS(rv, rv); 57 58 ShadowRoot* containingShadow = GetContainingShadow(); 59 mInManualShadowRoot = 60 containingShadow && 61 containingShadow->SlotAssignment() == SlotAssignmentMode::Manual; 62 if (containingShadow && !oldContainingShadow) { 63 containingShadow->AddSlot(this); 64 } 65 66 return NS_OK; 67 } 68 69 void HTMLSlotElement::UnbindFromTree(UnbindContext& aContext) { 70 RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow(); 71 72 nsGenericHTMLElement::UnbindFromTree(aContext); 73 74 if (oldContainingShadow && !GetContainingShadow()) { 75 oldContainingShadow->RemoveSlot(this); 76 } 77 } 78 79 void HTMLSlotElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, 80 const nsAttrValue* aValue, bool aNotify) { 81 if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::name) { 82 if (ShadowRoot* containingShadow = GetContainingShadow()) { 83 containingShadow->RemoveSlot(this); 84 } 85 } 86 87 return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, 88 aNotify); 89 } 90 91 void HTMLSlotElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, 92 const nsAttrValue* aValue, 93 const nsAttrValue* aOldValue, 94 nsIPrincipal* aSubjectPrincipal, 95 bool aNotify) { 96 if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::name) { 97 if (ShadowRoot* containingShadow = GetContainingShadow()) { 98 containingShadow->AddSlot(this); 99 } 100 } 101 102 return nsGenericHTMLElement::AfterSetAttr( 103 aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); 104 } 105 106 /** 107 * Flatten assigned nodes given a slot, as in: 108 * https://dom.spec.whatwg.org/#find-flattened-slotables 109 */ 110 static void FlattenAssignedNodes(HTMLSlotElement* aSlot, 111 nsTArray<RefPtr<nsINode>>& aNodes) { 112 if (!aSlot->GetContainingShadow()) { 113 return; 114 } 115 116 const Span<const RefPtr<nsINode>> assignedNodes = aSlot->AssignedNodes(); 117 118 // If assignedNodes is empty, use children of slot as fallback content. 119 if (assignedNodes.IsEmpty()) { 120 for (nsIContent* child = aSlot->GetFirstChild(); child; 121 child = child->GetNextSibling()) { 122 if (!child->IsSlotable()) { 123 continue; 124 } 125 126 if (auto* slot = HTMLSlotElement::FromNode(child)) { 127 FlattenAssignedNodes(slot, aNodes); 128 } else { 129 aNodes.AppendElement(child); 130 } 131 } 132 return; 133 } 134 135 for (const RefPtr<nsINode>& assignedNode : assignedNodes) { 136 auto* slot = HTMLSlotElement::FromNode(assignedNode); 137 if (slot && slot->GetContainingShadow()) { 138 FlattenAssignedNodes(slot, aNodes); 139 } else { 140 aNodes.AppendElement(assignedNode); 141 } 142 } 143 } 144 145 void HTMLSlotElement::AssignedNodes(const AssignedNodesOptions& aOptions, 146 nsTArray<RefPtr<nsINode>>& aNodes) { 147 if (aOptions.mFlatten) { 148 return FlattenAssignedNodes(this, aNodes); 149 } 150 151 aNodes.AppendElements(mAssignedNodes.AsSpan()); 152 } 153 154 void HTMLSlotElement::AssignedElements(const AssignedNodesOptions& aOptions, 155 nsTArray<RefPtr<Element>>& aElements) { 156 AutoTArray<RefPtr<nsINode>, 128> assignedNodes; 157 AssignedNodes(aOptions, assignedNodes); 158 for (const RefPtr<nsINode>& assignedNode : assignedNodes) { 159 if (assignedNode->IsElement()) { 160 aElements.AppendElement(assignedNode->AsElement()); 161 } 162 } 163 } 164 165 const nsTArray<nsINode*>& HTMLSlotElement::ManuallyAssignedNodes() const { 166 return mManuallyAssignedNodes; 167 } 168 169 void HTMLSlotElement::Assign(const Sequence<OwningElementOrText>& aNodes) { 170 nsAutoScriptBlocker scriptBlocker; 171 172 // no-op if the input nodes and the assigned nodes are identical 173 // This also works if the two 'assign' calls are like 174 // > slot.assign(node1, node2); 175 // > slot.assign(node1, node2, node1, node2); 176 if (!mAssignedNodes.IsEmpty() && aNodes.Length() >= mAssignedNodes.Length()) { 177 nsTHashMap<nsPtrHashKey<nsIContent>, size_t> nodeIndexMap; 178 for (size_t i = 0; i < aNodes.Length(); ++i) { 179 nsIContent* content; 180 if (aNodes[i].IsElement()) { 181 content = aNodes[i].GetAsElement(); 182 } else { 183 content = aNodes[i].GetAsText(); 184 } 185 MOZ_ASSERT(content); 186 // We only care about the first index this content appears 187 // in the array 188 nodeIndexMap.LookupOrInsert(content, i); 189 } 190 191 if (nodeIndexMap.Count() == mAssignedNodes.Length()) { 192 bool isIdentical = true; 193 for (size_t i = 0; i < mAssignedNodes.Length(); ++i) { 194 size_t indexInInputNodes; 195 if (!nodeIndexMap.Get(mAssignedNodes[i]->AsContent(), 196 &indexInInputNodes) || 197 indexInInputNodes != i) { 198 isIdentical = false; 199 break; 200 } 201 } 202 if (isIdentical) { 203 return; 204 } 205 } 206 } 207 208 // 1. For each node of this's manually assigned nodes, set node's manual slot 209 // assignment to null. 210 for (nsINode* node : mManuallyAssignedNodes) { 211 MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this); 212 node->AsContent()->SetManualSlotAssignment(nullptr); 213 } 214 215 // 2. Let nodesSet be a new ordered set. 216 mManuallyAssignedNodes.Clear(); 217 218 nsIContent* host = nullptr; 219 ShadowRoot* root = GetContainingShadow(); 220 221 // An optimization to keep track which slots need to enqueue 222 // slotchange event, such that they can be enqueued later in 223 // tree order. 224 nsTHashSet<RefPtr<HTMLSlotElement>> changedSlots; 225 226 // Clear out existing assigned nodes 227 if (mInManualShadowRoot) { 228 if (!mAssignedNodes.IsEmpty()) { 229 changedSlots.EnsureInserted(this); 230 if (root) { 231 root->InvalidateStyleAndLayoutOnSubtree(this); 232 } 233 ClearAssignedNodes(); 234 } 235 236 MOZ_ASSERT(mAssignedNodes.IsEmpty()); 237 host = GetContainingShadowHost(); 238 } 239 240 for (const OwningElementOrText& elementOrText : aNodes) { 241 nsIContent* content; 242 if (elementOrText.IsElement()) { 243 content = elementOrText.GetAsElement(); 244 } else { 245 content = elementOrText.GetAsText(); 246 } 247 248 MOZ_ASSERT(content); 249 // XXXsmaug Should we have a helper for 250 // https://infra.spec.whatwg.org/#ordered-set? 251 if (content->GetManualSlotAssignment() != this) { 252 if (HTMLSlotElement* oldSlot = content->GetAssignedSlot()) { 253 if (changedSlots.EnsureInserted(oldSlot)) { 254 if (root) { 255 MOZ_ASSERT(oldSlot->GetContainingShadow() == root); 256 root->InvalidateStyleAndLayoutOnSubtree(oldSlot); 257 } 258 } 259 } 260 261 if (changedSlots.EnsureInserted(this)) { 262 if (root) { 263 root->InvalidateStyleAndLayoutOnSubtree(this); 264 } 265 } 266 // 3.1 (HTML Spec) If content's manual slot assignment refers to a slot, 267 // then remove node from that slot's manually assigned nodes. 3.2 (HTML 268 // Spec) Set content's manual slot assignment to this. 269 if (HTMLSlotElement* oldSlot = content->GetManualSlotAssignment()) { 270 oldSlot->RemoveManuallyAssignedNode(*content); 271 } 272 content->SetManualSlotAssignment(this); 273 mManuallyAssignedNodes.AppendElement(content); 274 275 if (root && host && content->GetParent() == host) { 276 // Equivalent to 4.2.2.4.3 (DOM Spec) `Set slot's assigned nodes to 277 // slottables` 278 root->MaybeReassignContent(*content); 279 } 280 } 281 } 282 283 // The `assign slottables` step is completed already at this point, 284 // however we haven't fired the `slotchange` event yet because this 285 // needs to be done in tree order. 286 if (root) { 287 for (nsIContent* child = root->GetFirstChild(); child; 288 child = child->GetNextNode()) { 289 if (HTMLSlotElement* slot = HTMLSlotElement::FromNode(child)) { 290 if (changedSlots.EnsureRemoved(slot)) { 291 slot->EnqueueSlotChangeEvent(); 292 } 293 } 294 } 295 MOZ_ASSERT(changedSlots.IsEmpty()); 296 } 297 } 298 299 void HTMLSlotElement::InsertAssignedNode(uint32_t aIndex, nsIContent& aNode) { 300 MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot"); 301 mAssignedNodes.InsertElementAt(aIndex, &aNode); 302 aNode.SetAssignedSlot(this); 303 RecalculateHasSlottedState(); 304 SlotAssignedNodeAdded(this, aNode); 305 } 306 307 void HTMLSlotElement::AppendAssignedNode(nsIContent& aNode) { 308 MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot"); 309 mAssignedNodes.AppendElement(&aNode); 310 aNode.SetAssignedSlot(this); 311 RecalculateHasSlottedState(); 312 SlotAssignedNodeAdded(this, aNode); 313 } 314 315 void HTMLSlotElement::RecalculateHasSlottedState() { 316 bool hasSlotted = false; 317 // Find the first node that makes this a slotted element. 318 for (const RefPtr<nsINode>& assignedNode : mAssignedNodes.AsSpan()) { 319 if (auto* slot = HTMLSlotElement::FromNode(assignedNode)) { 320 if (slot->IsInShadowTree() && 321 !slot->State().HasState(ElementState::HAS_SLOTTED)) { 322 continue; 323 } 324 } 325 hasSlotted = true; 326 break; 327 } 328 if (State().HasState(ElementState::HAS_SLOTTED) != hasSlotted) { 329 SetStates(ElementState::HAS_SLOTTED, hasSlotted); 330 // If slot is a slotted node itself, the assigned slot needs to 331 // RecalculateHasSlottedState: 332 if (auto* slot = GetAssignedSlot()) { 333 slot->RecalculateHasSlottedState(); 334 } 335 } 336 } 337 338 void HTMLSlotElement::RemoveAssignedNode(nsIContent& aNode) { 339 // This one runs from unlinking, so we can't guarantee that the slot pointer 340 // hasn't been cleared. 341 MOZ_ASSERT(!aNode.GetAssignedSlot() || aNode.GetAssignedSlot() == this, 342 "How exactly?"); 343 mAssignedNodes.RemoveElement(&aNode); 344 aNode.SetAssignedSlot(nullptr); 345 346 RecalculateHasSlottedState(); 347 SlotAssignedNodeRemoved(this, aNode); 348 } 349 350 void HTMLSlotElement::ClearAssignedNodes() { 351 for (RefPtr<nsINode>& node : mAssignedNodes.AsSpan()) { 352 MOZ_ASSERT(!node->AsContent()->GetAssignedSlot() || 353 node->AsContent()->GetAssignedSlot() == this, 354 "How exactly?"); 355 node->AsContent()->SetAssignedSlot(nullptr); 356 } 357 358 mAssignedNodes.Clear(); 359 RecalculateHasSlottedState(); 360 } 361 362 void HTMLSlotElement::EnqueueSlotChangeEvent() { 363 if (mInSignalSlotList) { 364 return; 365 } 366 367 // FIXME(bug 1459704): Need to figure out how to deal with microtasks posted 368 // during shutdown. 369 if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { 370 return; 371 } 372 373 DocGroup* docGroup = OwnerDoc()->GetDocGroup(); 374 if (!docGroup) { 375 return; 376 } 377 378 mInSignalSlotList = true; 379 docGroup->SignalSlotChange(*this); 380 } 381 382 void HTMLSlotElement::FireSlotChangeEvent() { 383 nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"slotchange"_ns, 384 CanBubble::eYes, Cancelable::eNo); 385 } 386 387 void HTMLSlotElement::RemoveManuallyAssignedNode(nsIContent& aNode) { 388 mManuallyAssignedNodes.RemoveElement(&aNode); 389 RemoveAssignedNode(aNode); 390 } 391 392 JSObject* HTMLSlotElement::WrapNode(JSContext* aCx, 393 JS::Handle<JSObject*> aGivenProto) { 394 return HTMLSlotElement_Binding::Wrap(aCx, this, aGivenProto); 395 } 396 397 } // namespace mozilla::dom