Bug 1707290 [wpt PR 28671] - Auto-expand details elements for find-in-page, a=testonly
[gecko.git] / dom / xul / XULBroadcastManager.cpp
blob9246ad19bed46e14bccde60ca3210a2ae469a1fa
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=4 sw=2 et 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/. */
7 #include "XULBroadcastManager.h"
8 #include "nsCOMPtr.h"
9 #include "nsContentUtils.h"
10 #include "mozilla/EventDispatcher.h"
11 #include "mozilla/Logging.h"
12 #include "mozilla/dom/DocumentInlines.h"
13 #include "nsXULElement.h"
15 struct BroadcastListener {
16 nsWeakPtr mListener;
17 RefPtr<nsAtom> mAttribute;
20 struct BroadcasterMapEntry : public PLDHashEntryHdr {
21 mozilla::dom::Element* mBroadcaster; // [WEAK]
22 nsTArray<BroadcastListener*>
23 mListeners; // [OWNING] of BroadcastListener objects
26 struct nsAttrNameInfo {
27 nsAttrNameInfo(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix)
28 : mNamespaceID(aNamespaceID), mName(aName), mPrefix(aPrefix) {}
29 nsAttrNameInfo(const nsAttrNameInfo& aOther) = delete;
30 nsAttrNameInfo(nsAttrNameInfo&& aOther) = default;
32 int32_t mNamespaceID;
33 RefPtr<nsAtom> mName;
34 RefPtr<nsAtom> mPrefix;
37 static void ClearBroadcasterMapEntry(PLDHashTable* aTable,
38 PLDHashEntryHdr* aEntry) {
39 BroadcasterMapEntry* entry = static_cast<BroadcasterMapEntry*>(aEntry);
40 for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
41 delete entry->mListeners[i];
43 entry->mListeners.Clear();
45 // N.B. that we need to manually run the dtor because we
46 // constructed the nsTArray object in-place.
47 entry->mListeners.~nsTArray<BroadcastListener*>();
50 static bool CanBroadcast(int32_t aNameSpaceID, nsAtom* aAttribute) {
51 // Don't push changes to the |id|, |persist|, |command| or
52 // |observes| attribute.
53 if (aNameSpaceID == kNameSpaceID_None) {
54 if ((aAttribute == nsGkAtoms::id) || (aAttribute == nsGkAtoms::persist) ||
55 (aAttribute == nsGkAtoms::command) ||
56 (aAttribute == nsGkAtoms::observes)) {
57 return false;
60 return true;
63 namespace mozilla {
64 namespace dom {
65 static LazyLogModule sXULBroadCastManager("XULBroadcastManager");
67 class XULBroadcastManager::nsDelayedBroadcastUpdate {
68 public:
69 nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener,
70 const nsAString& aAttr)
71 : mBroadcaster(aBroadcaster),
72 mListener(aListener),
73 mAttr(aAttr),
74 mSetAttr(false),
75 mNeedsAttrChange(false) {}
77 nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener,
78 nsAtom* aAttrName, const nsAString& aAttr,
79 bool aSetAttr, bool aNeedsAttrChange)
80 : mBroadcaster(aBroadcaster),
81 mListener(aListener),
82 mAttr(aAttr),
83 mAttrName(aAttrName),
84 mSetAttr(aSetAttr),
85 mNeedsAttrChange(aNeedsAttrChange) {}
87 nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate& aOther) = delete;
88 nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate&& aOther) = default;
90 nsCOMPtr<Element> mBroadcaster;
91 nsCOMPtr<Element> mListener;
92 // Note if mAttrName isn't used, this is the name of the attr, otherwise
93 // this is the value of the attribute.
94 nsString mAttr;
95 RefPtr<nsAtom> mAttrName;
96 bool mSetAttr;
97 bool mNeedsAttrChange;
99 class Comparator {
100 public:
101 static bool Equals(const nsDelayedBroadcastUpdate& a,
102 const nsDelayedBroadcastUpdate& b) {
103 return a.mBroadcaster == b.mBroadcaster && a.mListener == b.mListener &&
104 a.mAttrName == b.mAttrName;
109 /* static */
110 bool XULBroadcastManager::MayNeedListener(const Element& aElement) {
111 if (aElement.NodeInfo()->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) {
112 return true;
114 if (aElement.HasAttr(nsGkAtoms::observes)) {
115 return true;
117 if (aElement.HasAttr(nsGkAtoms::command) &&
118 !(aElement.NodeInfo()->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) ||
119 aElement.NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL))) {
120 return true;
122 return false;
125 XULBroadcastManager::XULBroadcastManager(Document* aDocument)
126 : mDocument(aDocument),
127 mBroadcasterMap(nullptr),
128 mHandlingDelayedAttrChange(false),
129 mHandlingDelayedBroadcasters(false) {}
131 XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap; }
133 void XULBroadcastManager::DropDocumentReference(void) { mDocument = nullptr; }
135 void XULBroadcastManager::SynchronizeBroadcastListener(Element* aBroadcaster,
136 Element* aListener,
137 const nsAString& aAttr) {
138 if (!nsContentUtils::IsSafeToRunScript()) {
139 mDelayedBroadcasters.EmplaceBack(aBroadcaster, aListener, aAttr);
140 MaybeBroadcast();
141 return;
143 bool notify = mHandlingDelayedBroadcasters;
145 if (aAttr.EqualsLiteral("*")) {
146 uint32_t count = aBroadcaster->GetAttrCount();
147 nsTArray<nsAttrNameInfo> attributes(count);
148 for (uint32_t i = 0; i < count; ++i) {
149 const nsAttrName* attrName = aBroadcaster->GetAttrNameAt(i);
150 int32_t nameSpaceID = attrName->NamespaceID();
151 nsAtom* name = attrName->LocalName();
153 // _Don't_ push the |id|, |ref|, or |persist| attribute's value!
154 if (!CanBroadcast(nameSpaceID, name)) continue;
156 attributes.AppendElement(
157 nsAttrNameInfo(nameSpaceID, name, attrName->GetPrefix()));
160 count = attributes.Length();
161 while (count-- > 0) {
162 int32_t nameSpaceID = attributes[count].mNamespaceID;
163 nsAtom* name = attributes[count].mName;
164 nsAutoString value;
165 if (aBroadcaster->GetAttr(nameSpaceID, name, value)) {
166 aListener->SetAttr(nameSpaceID, name, attributes[count].mPrefix, value,
167 notify);
170 #if 0
171 // XXX we don't fire the |onbroadcast| handler during
172 // initial hookup: doing so would potentially run the
173 // |onbroadcast| handler before the |onload| handler,
174 // which could define JS properties that mask XBL
175 // properties, etc.
176 ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name);
177 #endif
179 } else {
180 // Find out if the attribute is even present at all.
181 RefPtr<nsAtom> name = NS_Atomize(aAttr);
183 nsAutoString value;
184 if (aBroadcaster->GetAttr(kNameSpaceID_None, name, value)) {
185 aListener->SetAttr(kNameSpaceID_None, name, value, notify);
186 } else {
187 aListener->UnsetAttr(kNameSpaceID_None, name, notify);
190 #if 0
191 // XXX we don't fire the |onbroadcast| handler during initial
192 // hookup: doing so would potentially run the |onbroadcast|
193 // handler before the |onload| handler, which could define JS
194 // properties that mask XBL properties, etc.
195 ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name);
196 #endif
200 void XULBroadcastManager::AddListenerFor(Element& aBroadcaster,
201 Element& aListener,
202 const nsAString& aAttr,
203 ErrorResult& aRv) {
204 if (!mDocument) {
205 aRv.Throw(NS_ERROR_FAILURE);
206 return;
209 nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, &aBroadcaster);
211 if (NS_FAILED(rv)) {
212 aRv.Throw(rv);
213 return;
216 rv = nsContentUtils::CheckSameOrigin(mDocument, &aListener);
218 if (NS_FAILED(rv)) {
219 aRv.Throw(rv);
220 return;
223 static const PLDHashTableOps gOps = {
224 PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub,
225 PLDHashTable::MoveEntryStub, ClearBroadcasterMapEntry, nullptr};
227 if (!mBroadcasterMap) {
228 mBroadcasterMap = new PLDHashTable(&gOps, sizeof(BroadcasterMapEntry));
231 auto entry =
232 static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster));
233 if (!entry) {
234 entry = static_cast<BroadcasterMapEntry*>(
235 mBroadcasterMap->Add(&aBroadcaster, fallible));
237 if (!entry) {
238 aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
239 return;
242 entry->mBroadcaster = &aBroadcaster;
244 // N.B. placement new to construct the nsTArray object in-place
245 new (&entry->mListeners) nsTArray<BroadcastListener*>();
248 // Only add the listener if it's not there already!
249 RefPtr<nsAtom> attr = NS_Atomize(aAttr);
251 for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
252 BroadcastListener* bl = entry->mListeners[i];
253 nsCOMPtr<Element> blListener = do_QueryReferent(bl->mListener);
255 if (blListener == &aListener && bl->mAttribute == attr) return;
258 BroadcastListener* bl = new BroadcastListener;
259 bl->mListener = do_GetWeakReference(&aListener);
260 bl->mAttribute = attr;
262 entry->mListeners.AppendElement(bl);
264 SynchronizeBroadcastListener(&aBroadcaster, &aListener, aAttr);
267 void XULBroadcastManager::RemoveListenerFor(Element& aBroadcaster,
268 Element& aListener,
269 const nsAString& aAttr) {
270 // If we haven't added any broadcast listeners, then there sure
271 // aren't any to remove.
272 if (!mBroadcasterMap) return;
274 auto entry =
275 static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster));
276 if (entry) {
277 RefPtr<nsAtom> attr = NS_Atomize(aAttr);
278 for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
279 BroadcastListener* bl = entry->mListeners[i];
280 nsCOMPtr<Element> blListener = do_QueryReferent(bl->mListener);
282 if (blListener == &aListener && bl->mAttribute == attr) {
283 entry->mListeners.RemoveElementAt(i);
284 delete bl;
286 if (entry->mListeners.IsEmpty()) mBroadcasterMap->RemoveEntry(entry);
288 break;
294 nsresult XULBroadcastManager::ExecuteOnBroadcastHandlerFor(
295 Element* aBroadcaster, Element* aListener, nsAtom* aAttr) {
296 if (!mDocument) {
297 return NS_OK;
299 // Now we execute the onchange handler in the context of the
300 // observer. We need to find the observer in order to
301 // execute the handler.
303 for (nsIContent* child = aListener->GetFirstChild(); child;
304 child = child->GetNextSibling()) {
305 // Look for an <observes> element beneath the listener. This
306 // ought to have an |element| attribute that refers to
307 // aBroadcaster, and an |attribute| element that tells us what
308 // attriubtes we're listening for.
309 if (!child->IsXULElement(nsGkAtoms::observes)) continue;
311 // Is this the element that was listening to us?
312 nsAutoString listeningToID;
313 child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::element,
314 listeningToID);
316 nsAutoString broadcasterID;
317 aBroadcaster->GetAttr(kNameSpaceID_None, nsGkAtoms::id, broadcasterID);
319 if (listeningToID != broadcasterID) continue;
321 // We are observing the broadcaster, but is this the right
322 // attribute?
323 nsAutoString listeningToAttribute;
324 child->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::attribute,
325 listeningToAttribute);
327 if (!aAttr->Equals(listeningToAttribute) &&
328 !listeningToAttribute.EqualsLiteral("*")) {
329 continue;
332 // This is the right <observes> element. Execute the
333 // |onbroadcast| event handler
334 WidgetEvent event(true, eXULBroadcast);
336 RefPtr<nsPresContext> presContext = mDocument->GetPresContext();
337 if (presContext) {
338 // Handle the DOM event
339 nsEventStatus status = nsEventStatus_eIgnore;
340 EventDispatcher::Dispatch(child, presContext, &event, nullptr, &status);
344 return NS_OK;
347 void XULBroadcastManager::AttributeChanged(Element* aElement,
348 int32_t aNameSpaceID,
349 nsAtom* aAttribute) {
350 if (!mDocument) {
351 return;
353 NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc");
355 // Synchronize broadcast listeners
356 if (mBroadcasterMap && CanBroadcast(aNameSpaceID, aAttribute)) {
357 auto entry =
358 static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(aElement));
360 if (entry) {
361 // We've got listeners: push the value.
362 nsAutoString value;
363 bool attrSet = aElement->GetAttr(kNameSpaceID_None, aAttribute, value);
365 for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
366 BroadcastListener* bl = entry->mListeners[i];
367 if ((bl->mAttribute == aAttribute) ||
368 (bl->mAttribute == nsGkAtoms::_asterisk)) {
369 nsCOMPtr<Element> listenerEl = do_QueryReferent(bl->mListener);
370 if (listenerEl) {
371 nsAutoString currentValue;
372 bool hasAttr = listenerEl->GetAttr(kNameSpaceID_None, aAttribute,
373 currentValue);
374 // We need to update listener only if we're
375 // (1) removing an existing attribute,
376 // (2) adding a new attribute or
377 // (3) changing the value of an attribute.
378 bool needsAttrChange =
379 attrSet != hasAttr || !value.Equals(currentValue);
380 nsDelayedBroadcastUpdate delayedUpdate(aElement, listenerEl,
381 aAttribute, value, attrSet,
382 needsAttrChange);
384 size_t index = mDelayedAttrChangeBroadcasts.IndexOf(
385 delayedUpdate, 0, nsDelayedBroadcastUpdate::Comparator());
386 if (index != mDelayedAttrChangeBroadcasts.NoIndex) {
387 if (mHandlingDelayedAttrChange) {
388 NS_WARNING("Broadcasting loop!");
389 continue;
391 mDelayedAttrChangeBroadcasts.RemoveElementAt(index);
394 mDelayedAttrChangeBroadcasts.AppendElement(
395 std::move(delayedUpdate));
403 void XULBroadcastManager::MaybeBroadcast() {
404 // Only broadcast when not in an update and when safe to run scripts.
405 if (mDocument && mDocument->UpdateNestingLevel() == 0 &&
406 (mDelayedAttrChangeBroadcasts.Length() ||
407 mDelayedBroadcasters.Length())) {
408 if (!nsContentUtils::IsSafeToRunScript()) {
409 if (mDocument) {
410 nsContentUtils::AddScriptRunner(
411 NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this,
412 &XULBroadcastManager::MaybeBroadcast));
414 return;
416 if (!mHandlingDelayedAttrChange) {
417 mHandlingDelayedAttrChange = true;
418 for (uint32_t i = 0; i < mDelayedAttrChangeBroadcasts.Length(); ++i) {
419 nsAtom* attrName = mDelayedAttrChangeBroadcasts[i].mAttrName;
420 if (mDelayedAttrChangeBroadcasts[i].mNeedsAttrChange) {
421 nsCOMPtr<Element> listener =
422 mDelayedAttrChangeBroadcasts[i].mListener;
423 const nsString& value = mDelayedAttrChangeBroadcasts[i].mAttr;
424 if (mDelayedAttrChangeBroadcasts[i].mSetAttr) {
425 listener->SetAttr(kNameSpaceID_None, attrName, value, true);
426 } else {
427 listener->UnsetAttr(kNameSpaceID_None, attrName, true);
430 ExecuteOnBroadcastHandlerFor(
431 mDelayedAttrChangeBroadcasts[i].mBroadcaster,
432 mDelayedAttrChangeBroadcasts[i].mListener, attrName);
434 mDelayedAttrChangeBroadcasts.Clear();
435 mHandlingDelayedAttrChange = false;
438 uint32_t length = mDelayedBroadcasters.Length();
439 if (length) {
440 bool oldValue = mHandlingDelayedBroadcasters;
441 mHandlingDelayedBroadcasters = true;
442 nsTArray<nsDelayedBroadcastUpdate> delayedBroadcasters =
443 std::move(mDelayedBroadcasters);
444 for (uint32_t i = 0; i < length; ++i) {
445 SynchronizeBroadcastListener(delayedBroadcasters[i].mBroadcaster,
446 delayedBroadcasters[i].mListener,
447 delayedBroadcasters[i].mAttr);
449 mHandlingDelayedBroadcasters = oldValue;
454 nsresult XULBroadcastManager::FindBroadcaster(Element* aElement,
455 Element** aListener,
456 nsString& aBroadcasterID,
457 nsString& aAttribute,
458 Element** aBroadcaster) {
459 NodeInfo* ni = aElement->NodeInfo();
460 *aListener = nullptr;
461 *aBroadcaster = nullptr;
463 if (ni->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) {
464 // It's an <observes> element, which means that the actual
465 // listener is the _parent_ node. This element should have an
466 // 'element' attribute that specifies the ID of the
467 // broadcaster element, and an 'attribute' element, which
468 // specifies the name of the attribute to observe.
469 nsIContent* parent = aElement->GetParent();
470 if (!parent) {
471 // <observes> is the root element
472 return NS_FINDBROADCASTER_NOT_FOUND;
475 *aListener = Element::FromNode(parent);
476 NS_IF_ADDREF(*aListener);
478 aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::element, aBroadcasterID);
479 if (aBroadcasterID.IsEmpty()) {
480 return NS_FINDBROADCASTER_NOT_FOUND;
482 aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::attribute, aAttribute);
483 } else {
484 // It's a generic element, which means that we'll use the
485 // value of the 'observes' attribute to determine the ID of
486 // the broadcaster element, and we'll watch _all_ of its
487 // values.
488 aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::observes, aBroadcasterID);
490 // Bail if there's no aBroadcasterID
491 if (aBroadcasterID.IsEmpty()) {
492 // Try the command attribute next.
493 aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command, aBroadcasterID);
494 if (!aBroadcasterID.IsEmpty()) {
495 // We've got something in the command attribute. We
496 // only treat this as a normal broadcaster if we are
497 // not a menuitem or a key.
499 if (ni->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) ||
500 ni->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) {
501 return NS_FINDBROADCASTER_NOT_FOUND;
503 } else {
504 return NS_FINDBROADCASTER_NOT_FOUND;
508 *aListener = aElement;
509 NS_ADDREF(*aListener);
511 aAttribute.Assign('*');
514 // Make sure we got a valid listener.
515 NS_ENSURE_TRUE(*aListener, NS_ERROR_UNEXPECTED);
517 // Try to find the broadcaster element in the document.
518 Document* doc = aElement->GetComposedDoc();
519 if (doc) {
520 *aBroadcaster = doc->GetElementById(aBroadcasterID);
523 // The broadcaster element is missing.
524 if (!*aBroadcaster) {
525 return NS_FINDBROADCASTER_NOT_FOUND;
528 NS_ADDREF(*aBroadcaster);
530 return NS_FINDBROADCASTER_FOUND;
533 nsresult XULBroadcastManager::UpdateListenerHookup(Element* aElement,
534 HookupAction aAction) {
535 // Resolve a broadcaster hookup. Look at the element that we're
536 // trying to resolve: it could be an '<observes>' element, or just
537 // a vanilla element with an 'observes' attribute on it.
538 nsresult rv;
540 nsCOMPtr<Element> listener;
541 nsAutoString broadcasterID;
542 nsAutoString attribute;
543 nsCOMPtr<Element> broadcaster;
545 rv = FindBroadcaster(aElement, getter_AddRefs(listener), broadcasterID,
546 attribute, getter_AddRefs(broadcaster));
547 switch (rv) {
548 case NS_FINDBROADCASTER_NOT_FOUND:
549 return NS_OK;
550 case NS_FINDBROADCASTER_FOUND:
551 break;
552 default:
553 return rv;
556 NS_ENSURE_ARG(broadcaster && listener);
557 if (aAction == eHookupAdd) {
558 ErrorResult domRv;
559 AddListenerFor(*broadcaster, *listener, attribute, domRv);
560 if (domRv.Failed()) {
561 return domRv.StealNSResult();
563 } else {
564 RemoveListenerFor(*broadcaster, *listener, attribute);
567 // Tell the world we succeeded
568 if (MOZ_LOG_TEST(sXULBroadCastManager, LogLevel::Debug)) {
569 nsCOMPtr<nsIContent> content = listener;
570 NS_ASSERTION(content != nullptr, "not an nsIContent");
571 if (!content) {
572 return rv;
575 nsAutoCString attributeC, broadcasteridC;
576 LossyCopyUTF16toASCII(attribute, attributeC);
577 LossyCopyUTF16toASCII(broadcasterID, broadcasteridC);
578 MOZ_LOG(sXULBroadCastManager, LogLevel::Debug,
579 ("xul: broadcaster hookup <%s attribute='%s'> to %s",
580 nsAtomCString(content->NodeInfo()->NameAtom()).get(),
581 attributeC.get(), broadcasteridC.get()));
584 return NS_OK;
587 nsresult XULBroadcastManager::AddListener(Element* aElement) {
588 return UpdateListenerHookup(aElement, eHookupAdd);
591 nsresult XULBroadcastManager::RemoveListener(Element* aElement) {
592 return UpdateListenerHookup(aElement, eHookupRemove);
595 } // namespace dom
596 } // namespace mozilla