1 | /* |
2 | * Copyright (C) 2015 Apple Inc. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions |
6 | * are met: |
7 | * 1. Redistributions of source code must retain the above copyright |
8 | * notice, this list of conditions and the following disclaimer. |
9 | * 2. Redistributions in binary form must reproduce the above copyright |
10 | * notice, this list of conditions and the following disclaimer in the |
11 | * documentation and/or other materials provided with the distribution. |
12 | * |
13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
18 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
20 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
21 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
24 | */ |
25 | |
26 | #include "config.h" |
27 | #include "SlotAssignment.h" |
28 | |
29 | |
30 | #include "HTMLSlotElement.h" |
31 | #include "ShadowRoot.h" |
32 | #include "TypedElementDescendantIterator.h" |
33 | |
34 | namespace WebCore { |
35 | |
36 | using namespace HTMLNames; |
37 | |
38 | static const AtomicString& slotNameFromAttributeValue(const AtomicString& value) |
39 | { |
40 | return value == nullAtom() ? SlotAssignment::defaultSlotName() : value; |
41 | } |
42 | |
43 | static const AtomicString& slotNameFromSlotAttribute(const Node& child) |
44 | { |
45 | if (is<Text>(child)) |
46 | return SlotAssignment::defaultSlotName(); |
47 | |
48 | return slotNameFromAttributeValue(downcast<Element>(child).attributeWithoutSynchronization(slotAttr)); |
49 | } |
50 | |
51 | #if !ASSERT_DISABLED |
52 | static HTMLSlotElement* findSlotElement(ShadowRoot& shadowRoot, const AtomicString& slotName) |
53 | { |
54 | for (auto& slotElement : descendantsOfType<HTMLSlotElement>(shadowRoot)) { |
55 | if (slotNameFromAttributeValue(slotElement.attributeWithoutSynchronization(nameAttr)) == slotName) |
56 | return &slotElement; |
57 | } |
58 | return nullptr; |
59 | } |
60 | #endif |
61 | |
62 | static HTMLSlotElement* nextSlotElementSkippingSubtree(ContainerNode& startingNode, ContainerNode* skippedSubtree) |
63 | { |
64 | Node* node = &startingNode; |
65 | do { |
66 | if (UNLIKELY(node == skippedSubtree)) |
67 | node = NodeTraversal::nextSkippingChildren(*node); |
68 | else |
69 | node = NodeTraversal::next(*node); |
70 | } while (node && !is<HTMLSlotElement>(node)); |
71 | return downcast<HTMLSlotElement>(node); |
72 | } |
73 | |
74 | SlotAssignment::SlotAssignment() = default; |
75 | |
76 | SlotAssignment::~SlotAssignment() = default; |
77 | |
78 | HTMLSlotElement* SlotAssignment::findAssignedSlot(const Node& node, ShadowRoot& shadowRoot) |
79 | { |
80 | if (!is<Text>(node) && !is<Element>(node)) |
81 | return nullptr; |
82 | |
83 | auto* slot = m_slots.get(slotNameForHostChild(node)); |
84 | if (!slot) |
85 | return nullptr; |
86 | |
87 | return findFirstSlotElement(*slot, shadowRoot); |
88 | } |
89 | |
90 | inline bool SlotAssignment::hasAssignedNodes(ShadowRoot& shadowRoot, Slot& slot) |
91 | { |
92 | if (!m_slotAssignmentsIsValid) |
93 | assignSlots(shadowRoot); |
94 | return !slot.assignedNodes.isEmpty(); |
95 | } |
96 | |
97 | void SlotAssignment::renameSlotElement(HTMLSlotElement& slotElement, const AtomicString& oldName, const AtomicString& newName, ShadowRoot& shadowRoot) |
98 | { |
99 | ASSERT(m_slotElementsForConsistencyCheck.contains(&slotElement)); |
100 | |
101 | m_slotMutationVersion++; |
102 | |
103 | removeSlotElementByName(oldName, slotElement, nullptr, shadowRoot); |
104 | addSlotElementByName(newName, slotElement, shadowRoot); |
105 | } |
106 | |
107 | void SlotAssignment::addSlotElementByName(const AtomicString& name, HTMLSlotElement& slotElement, ShadowRoot& shadowRoot) |
108 | { |
109 | #ifndef NDEBUG |
110 | ASSERT(!m_slotElementsForConsistencyCheck.contains(&slotElement)); |
111 | m_slotElementsForConsistencyCheck.add(&slotElement); |
112 | #endif |
113 | |
114 | // FIXME: We should be able to do a targeted reconstruction. |
115 | shadowRoot.host()->invalidateStyleAndRenderersForSubtree(); |
116 | |
117 | auto& slotName = slotNameFromAttributeValue(name); |
118 | auto addResult = m_slots.ensure(slotName, [&] { |
119 | // Unlike named slots, assignSlots doesn't collect nodes assigned to the default slot |
120 | // to avoid always having a vector of all child nodes of a shadow host. |
121 | if (slotName == defaultSlotName()) |
122 | m_slotAssignmentsIsValid = false; |
123 | return std::make_unique<Slot>(); |
124 | }); |
125 | auto& slot = *addResult.iterator->value; |
126 | bool needsSlotchangeEvent = shadowRoot.shouldFireSlotchangeEvent() && hasAssignedNodes(shadowRoot, slot); |
127 | |
128 | slot.elementCount++; |
129 | if (slot.elementCount == 1) { |
130 | slot.element = makeWeakPtr(slotElement); |
131 | if (needsSlotchangeEvent) |
132 | slotElement.enqueueSlotChangeEvent(); |
133 | return; |
134 | } |
135 | |
136 | if (!needsSlotchangeEvent) { |
137 | ASSERT(slot.element || m_needsToResolveSlotElements); |
138 | slot.element = nullptr; |
139 | m_needsToResolveSlotElements = true; |
140 | return; |
141 | } |
142 | |
143 | resolveSlotsAfterSlotMutation(shadowRoot, SlotMutationType::Insertion); |
144 | } |
145 | |
146 | void SlotAssignment::removeSlotElementByName(const AtomicString& name, HTMLSlotElement& slotElement, ContainerNode* oldParentOfRemovedTreeForRemoval, ShadowRoot& shadowRoot) |
147 | { |
148 | #ifndef NDEBUG |
149 | ASSERT(m_slotElementsForConsistencyCheck.contains(&slotElement)); |
150 | m_slotElementsForConsistencyCheck.remove(&slotElement); |
151 | #endif |
152 | |
153 | if (auto* host = shadowRoot.host()) // FIXME: We should be able to do a targeted reconstruction. |
154 | host->invalidateStyleAndRenderersForSubtree(); |
155 | |
156 | auto* slot = m_slots.get(slotNameFromAttributeValue(name)); |
157 | RELEASE_ASSERT(slot && slot->hasSlotElements()); |
158 | bool needsSlotchangeEvent = shadowRoot.shouldFireSlotchangeEvent() && hasAssignedNodes(shadowRoot, *slot); |
159 | |
160 | slot->elementCount--; |
161 | if (!slot->elementCount) { |
162 | slot->element = nullptr; |
163 | if (needsSlotchangeEvent && m_slotResolutionVersion != m_slotMutationVersion) |
164 | slotElement.enqueueSlotChangeEvent(); |
165 | return; |
166 | } |
167 | |
168 | if (!needsSlotchangeEvent) { |
169 | ASSERT(slot->element || m_needsToResolveSlotElements); |
170 | slot->element = nullptr; |
171 | m_needsToResolveSlotElements = true; |
172 | return; |
173 | } |
174 | |
175 | bool elementWasRenamed = !oldParentOfRemovedTreeForRemoval; |
176 | if (elementWasRenamed && slot->element == &slotElement) |
177 | slotElement.enqueueSlotChangeEvent(); |
178 | |
179 | // A previous invocation to resolveSlotsAfterSlotMutation during this removal has updated this slot. |
180 | ASSERT(slot->element || (m_slotResolutionVersion == m_slotMutationVersion && !findSlotElement(shadowRoot, name))); |
181 | if (slot->element) { |
182 | resolveSlotsAfterSlotMutation(shadowRoot, elementWasRenamed ? SlotMutationType::Insertion : SlotMutationType::Removal, |
183 | m_willBeRemovingAllChildren ? oldParentOfRemovedTreeForRemoval : nullptr); |
184 | } |
185 | |
186 | if (slot->oldElement == &slotElement) { |
187 | slotElement.enqueueSlotChangeEvent(); |
188 | slot->oldElement = nullptr; |
189 | } |
190 | } |
191 | |
192 | void SlotAssignment::resolveSlotsAfterSlotMutation(ShadowRoot& shadowRoot, SlotMutationType mutationType, ContainerNode* subtreeToSkip) |
193 | { |
194 | if (m_slotResolutionVersion == m_slotMutationVersion) |
195 | return; |
196 | m_slotResolutionVersion = m_slotMutationVersion; |
197 | |
198 | ASSERT(!subtreeToSkip || mutationType == SlotMutationType::Removal); |
199 | m_needsToResolveSlotElements = false; |
200 | |
201 | for (auto& slot : m_slots.values()) |
202 | slot->seenFirstElement = false; |
203 | |
204 | unsigned slotCount = 0; |
205 | HTMLSlotElement* currentElement = nextSlotElementSkippingSubtree(shadowRoot, subtreeToSkip); |
206 | for (; currentElement; currentElement = nextSlotElementSkippingSubtree(*currentElement, subtreeToSkip)) { |
207 | auto& currentSlotName = slotNameFromAttributeValue(currentElement->attributeWithoutSynchronization(nameAttr)); |
208 | auto* currentSlot = m_slots.get(currentSlotName); |
209 | if (!currentSlot) { |
210 | // A new slot may have been inserted with this node but appears later in the tree order. |
211 | // Such a slot would go through the fast path in addSlotElementByName, |
212 | // and any subsequently inserted slot of the same name would not result in any slotchange or invokation of this function. |
213 | ASSERT(mutationType == SlotMutationType::Insertion); |
214 | continue; |
215 | } |
216 | if (currentSlot->seenFirstElement) { |
217 | if (mutationType == SlotMutationType::Insertion && currentSlot->oldElement == currentElement) { |
218 | currentElement->enqueueSlotChangeEvent(); |
219 | currentSlot->oldElement = nullptr; |
220 | } |
221 | continue; |
222 | } |
223 | currentSlot->seenFirstElement = true; |
224 | slotCount++; |
225 | ASSERT(currentSlot->element || !hasAssignedNodes(shadowRoot, *currentSlot)); |
226 | if (currentSlot->element != currentElement) { |
227 | if (hasAssignedNodes(shadowRoot, *currentSlot)) { |
228 | currentSlot->oldElement = WTFMove(currentSlot->element); |
229 | currentElement->enqueueSlotChangeEvent(); |
230 | } |
231 | currentSlot->element = makeWeakPtr(*currentElement); |
232 | } |
233 | } |
234 | |
235 | if (slotCount == m_slots.size()) |
236 | return; |
237 | |
238 | if (mutationType == SlotMutationType::Insertion) { |
239 | // This code path is taken only when continue above for !currentSlot is taken. |
240 | // i.e. there is a new slot being inserted into the tree but we have yet to invoke addSlotElementByName on it. |
241 | #if !ASSERT_DISABLED |
242 | for (auto& entry : m_slots) |
243 | ASSERT(entry.value->seenFirstElement || !findSlotElement(shadowRoot, entry.key)); |
244 | #endif |
245 | return; |
246 | } |
247 | |
248 | for (auto& slot : m_slots.values()) { |
249 | if (slot->seenFirstElement) |
250 | continue; |
251 | if (!slot->elementCount) { |
252 | // Taken the fast path for removal. |
253 | ASSERT(!slot->element); |
254 | continue; |
255 | } |
256 | // All slot elements have been removed for this slot. |
257 | slot->seenFirstElement = true; |
258 | ASSERT(slot->element); |
259 | if (hasAssignedNodes(shadowRoot, *slot)) |
260 | slot->oldElement = WTFMove(slot->element); |
261 | slot->element = nullptr; |
262 | } |
263 | } |
264 | |
265 | void SlotAssignment::slotFallbackDidChange(HTMLSlotElement& slotElement, ShadowRoot& shadowRoot) |
266 | { |
267 | if (shadowRoot.mode() == ShadowRootMode::UserAgent) |
268 | return; |
269 | |
270 | bool usesFallbackContent = !assignedNodesForSlot(slotElement, shadowRoot); |
271 | if (usesFallbackContent) |
272 | slotElement.enqueueSlotChangeEvent(); |
273 | } |
274 | |
275 | void SlotAssignment::resolveSlotsBeforeNodeInsertionOrRemoval(ShadowRoot& shadowRoot) |
276 | { |
277 | ASSERT(shadowRoot.shouldFireSlotchangeEvent()); |
278 | m_slotMutationVersion++; |
279 | m_willBeRemovingAllChildren = false; |
280 | if (m_needsToResolveSlotElements) |
281 | resolveAllSlotElements(shadowRoot); |
282 | } |
283 | |
284 | void SlotAssignment::willRemoveAllChildren(ShadowRoot& shadowRoot) |
285 | { |
286 | m_slotMutationVersion++; |
287 | m_willBeRemovingAllChildren = true; |
288 | if (m_needsToResolveSlotElements) |
289 | resolveAllSlotElements(shadowRoot); |
290 | } |
291 | |
292 | void SlotAssignment::didChangeSlot(const AtomicString& slotAttrValue, ShadowRoot& shadowRoot) |
293 | { |
294 | auto& slotName = slotNameFromAttributeValue(slotAttrValue); |
295 | auto* slot = m_slots.get(slotName); |
296 | if (!slot) |
297 | return; |
298 | |
299 | slot->assignedNodes.clear(); |
300 | m_slotAssignmentsIsValid = false; |
301 | |
302 | auto slotElement = makeRefPtr(findFirstSlotElement(*slot, shadowRoot)); |
303 | if (!slotElement) |
304 | return; |
305 | |
306 | shadowRoot.host()->invalidateStyleAndRenderersForSubtree(); |
307 | |
308 | if (shadowRoot.shouldFireSlotchangeEvent()) |
309 | slotElement->enqueueSlotChangeEvent(); |
310 | } |
311 | |
312 | void SlotAssignment::hostChildElementDidChange(const Element& childElement, ShadowRoot& shadowRoot) |
313 | { |
314 | didChangeSlot(childElement.attributeWithoutSynchronization(slotAttr), shadowRoot); |
315 | } |
316 | |
317 | const Vector<Node*>* SlotAssignment::assignedNodesForSlot(const HTMLSlotElement& slotElement, ShadowRoot& shadowRoot) |
318 | { |
319 | ASSERT(slotElement.containingShadowRoot() == &shadowRoot); |
320 | const AtomicString& slotName = slotNameFromAttributeValue(slotElement.attributeWithoutSynchronization(nameAttr)); |
321 | auto* slot = m_slots.get(slotName); |
322 | RELEASE_ASSERT(slot); |
323 | |
324 | if (!m_slotAssignmentsIsValid) |
325 | assignSlots(shadowRoot); |
326 | |
327 | if (slot->assignedNodes.isEmpty()) |
328 | return nullptr; |
329 | |
330 | RELEASE_ASSERT(slot->hasSlotElements()); |
331 | if (slot->hasDuplicatedSlotElements() && findFirstSlotElement(*slot, shadowRoot) != &slotElement) |
332 | return nullptr; |
333 | |
334 | return &slot->assignedNodes; |
335 | } |
336 | |
337 | const AtomicString& SlotAssignment::slotNameForHostChild(const Node& child) const |
338 | { |
339 | return slotNameFromSlotAttribute(child); |
340 | } |
341 | |
342 | HTMLSlotElement* SlotAssignment::findFirstSlotElement(Slot& slot, ShadowRoot& shadowRoot) |
343 | { |
344 | if (slot.shouldResolveSlotElement()) |
345 | resolveAllSlotElements(shadowRoot); |
346 | |
347 | #ifndef NDEBUG |
348 | ASSERT(!slot.element || m_slotElementsForConsistencyCheck.contains(slot.element.get())); |
349 | ASSERT(!!slot.element == !!slot.elementCount); |
350 | #endif |
351 | |
352 | return slot.element.get(); |
353 | } |
354 | |
355 | void SlotAssignment::resolveAllSlotElements(ShadowRoot& shadowRoot) |
356 | { |
357 | ASSERT(m_needsToResolveSlotElements); |
358 | m_needsToResolveSlotElements = false; |
359 | |
360 | // FIXME: It's inefficient to reset all values. We should be able to void this in common case. |
361 | for (auto& entry : m_slots) |
362 | entry.value->seenFirstElement = false; |
363 | |
364 | unsigned slotCount = m_slots.size(); |
365 | for (auto& slotElement : descendantsOfType<HTMLSlotElement>(shadowRoot)) { |
366 | auto& slotName = slotNameFromAttributeValue(slotElement.attributeWithoutSynchronization(nameAttr)); |
367 | |
368 | auto* slot = m_slots.get(slotName); |
369 | RELEASE_ASSERT(slot); // slot must have been created when a slot was inserted. |
370 | |
371 | if (slot->seenFirstElement) |
372 | continue; |
373 | slot->seenFirstElement = true; |
374 | |
375 | slot->element = makeWeakPtr(slotElement); |
376 | slotCount--; |
377 | if (!slotCount) |
378 | break; |
379 | } |
380 | } |
381 | |
382 | void SlotAssignment::assignSlots(ShadowRoot& shadowRoot) |
383 | { |
384 | ASSERT(!m_slotAssignmentsIsValid); |
385 | m_slotAssignmentsIsValid = true; |
386 | |
387 | for (auto& entry : m_slots) |
388 | entry.value->assignedNodes.shrink(0); |
389 | |
390 | auto& host = *shadowRoot.host(); |
391 | for (auto* child = host.firstChild(); child; child = child->nextSibling()) { |
392 | if (!is<Text>(*child) && !is<Element>(*child)) |
393 | continue; |
394 | auto slotName = slotNameForHostChild(*child); |
395 | assignToSlot(*child, slotName); |
396 | } |
397 | |
398 | for (auto& entry : m_slots) |
399 | entry.value->assignedNodes.shrinkToFit(); |
400 | } |
401 | |
402 | void SlotAssignment::assignToSlot(Node& child, const AtomicString& slotName) |
403 | { |
404 | ASSERT(!slotName.isNull()); |
405 | if (slotName == defaultSlotName()) { |
406 | auto defaultSlotEntry = m_slots.find(defaultSlotName()); |
407 | if (defaultSlotEntry != m_slots.end()) |
408 | defaultSlotEntry->value->assignedNodes.append(&child); |
409 | return; |
410 | } |
411 | |
412 | auto addResult = m_slots.ensure(slotName, [] { |
413 | return std::make_unique<Slot>(); |
414 | }); |
415 | addResult.iterator->value->assignedNodes.append(&child); |
416 | } |
417 | |
418 | } |
419 | |
420 | |
421 | |