Bug 1861467 - [wpt-sync] Update web-platform-tests to eedf737ce39c512d0ca3471f988972e...
[gecko.git] / dom / l10n / L10nMutations.cpp
blob03a4afaa5b18f859834c15b2e089b659eedffb75
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/. */
7 #include "L10nMutations.h"
8 #include "mozilla/dom/DocumentInlines.h"
9 #include "nsRefreshDriver.h"
10 #include "DOMLocalization.h"
11 #include "mozilla/intl/Localization.h"
12 #include "nsThreadManager.h"
14 using namespace mozilla;
15 using namespace mozilla::intl;
16 using namespace mozilla::dom;
18 NS_IMPL_CYCLE_COLLECTION_CLASS(L10nMutations)
20 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations)
21 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElements)
22 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElementsHash)
23 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
25 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(L10nMutations)
26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElements)
27 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElementsHash)
28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
30 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutations)
31 NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
32 NS_INTERFACE_MAP_ENTRY(nsISupports)
33 NS_INTERFACE_MAP_END
35 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations)
36 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations)
38 L10nMutations::L10nMutations(DOMLocalization* aDOMLocalization)
39 : mDOMLocalization(aDOMLocalization) {
40 mObserving = true;
43 L10nMutations::~L10nMutations() {
44 StopRefreshObserver();
45 MOZ_ASSERT(!mDOMLocalization,
46 "DOMLocalization<-->L10nMutations cycle should be broken.");
49 void L10nMutations::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
50 nsAtom* aAttribute, int32_t aModType,
51 const nsAttrValue* aOldValue) {
52 if (!mObserving) {
53 return;
56 if (aNameSpaceID == kNameSpaceID_None &&
57 (aAttribute == nsGkAtoms::datal10nid ||
58 aAttribute == nsGkAtoms::datal10nargs)) {
59 if (IsInRoots(aElement)) {
60 L10nElementChanged(aElement);
65 void L10nMutations::ContentAppended(nsIContent* aChild) {
66 if (!mObserving) {
67 return;
70 if (!IsInRoots(aChild)) {
71 return;
74 Sequence<OwningNonNull<Element>> elements;
75 for (nsIContent* node = aChild; node; node = node->GetNextSibling()) {
76 if (node->IsElement()) {
77 DOMLocalization::GetTranslatables(*node, elements, IgnoreErrors());
81 for (auto& elem : elements) {
82 L10nElementChanged(elem);
86 void L10nMutations::ContentInserted(nsIContent* aChild) {
87 if (!mObserving) {
88 return;
91 if (!aChild->IsElement()) {
92 return;
94 Element* elem = aChild->AsElement();
96 if (!IsInRoots(elem)) {
97 return;
100 Sequence<OwningNonNull<Element>> elements;
101 DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors());
103 for (auto& elem : elements) {
104 L10nElementChanged(elem);
108 void L10nMutations::ContentRemoved(nsIContent* aChild,
109 nsIContent* aPreviousSibling) {
110 if (!mObserving || mPendingElements.IsEmpty()) {
111 return;
114 Element* elem = Element::FromNode(*aChild);
115 if (!elem || !IsInRoots(elem)) {
116 return;
119 Sequence<OwningNonNull<Element>> elements;
120 DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors());
122 for (auto& elem : elements) {
123 if (mPendingElementsHash.EnsureRemoved(elem)) {
124 mPendingElements.RemoveElement(elem);
128 if (!HasPendingMutations()) {
129 nsContentUtils::AddScriptRunner(NewRunnableMethod(
130 "MaybeFirePendingTranslationsFinished", this,
131 &L10nMutations::MaybeFirePendingTranslationsFinished));
135 void L10nMutations::L10nElementChanged(Element* aElement) {
136 const bool wasEmpty = mPendingElements.IsEmpty();
138 if (mPendingElementsHash.EnsureInserted(aElement)) {
139 mPendingElements.AppendElement(aElement);
142 if (!wasEmpty) {
143 return;
146 if (!mRefreshDriver) {
147 StartRefreshObserver();
150 if (!mBlockingLoad) {
151 Document* doc = GetDocument();
152 if (doc && doc->GetReadyStateEnum() != Document::READYSTATE_COMPLETE) {
153 doc->BlockOnload();
154 mBlockingLoad = true;
158 if (mBlockingLoad && !mPendingBlockingLoadFlush) {
159 // We want to make sure we flush translations and don't block the load
160 // indefinitely (and, in fact, that we do it rather soon, even if the
161 // refresh driver is not ticking yet).
163 // In some platforms (mainly Wayland) the load of the main document
164 // causes vsync to start running and start ticking the refresh driver,
165 // so we can't rely on the refresh driver ticking yet.
166 RefPtr<nsIRunnable> task =
167 NewRunnableMethod("FlushPendingTranslationsBeforeLoad", this,
168 &L10nMutations::FlushPendingTranslationsBeforeLoad);
169 nsThreadManager::get().DispatchDirectTaskToCurrentThread(task);
170 mPendingBlockingLoadFlush = true;
174 void L10nMutations::PauseObserving() { mObserving = false; }
176 void L10nMutations::ResumeObserving() { mObserving = true; }
178 void L10nMutations::WillRefresh(mozilla::TimeStamp aTime) {
179 StopRefreshObserver();
180 FlushPendingTranslations();
184 * The handler for the `TranslateElements` promise used to turn
185 * a potential rejection into a console warning.
187 class L10nMutationFinalizationHandler final : public PromiseNativeHandler {
188 public:
189 NS_DECL_CYCLE_COLLECTING_ISUPPORTS
190 NS_DECL_CYCLE_COLLECTION_CLASS(L10nMutationFinalizationHandler)
192 explicit L10nMutationFinalizationHandler(L10nMutations* aMutations,
193 nsIGlobalObject* aGlobal)
194 : mMutations(aMutations), mGlobal(aGlobal) {}
196 MOZ_CAN_RUN_SCRIPT void Settled() {
197 if (RefPtr mutations = mMutations) {
198 mutations->PendingPromiseSettled();
202 MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx,
203 JS::Handle<JS::Value> aValue,
204 ErrorResult& aRv) override {
205 Settled();
208 MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx,
209 JS::Handle<JS::Value> aValue,
210 ErrorResult& aRv) override {
211 nsTArray<nsCString> errors{
212 "[dom/l10n] Errors during l10n mutation frame."_ns,
214 MaybeReportErrorsToGecko(errors, IgnoreErrors(), mGlobal);
215 Settled();
218 private:
219 ~L10nMutationFinalizationHandler() = default;
221 RefPtr<L10nMutations> mMutations;
222 nsCOMPtr<nsIGlobalObject> mGlobal;
225 NS_IMPL_CYCLE_COLLECTION(L10nMutationFinalizationHandler, mGlobal, mMutations)
227 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutationFinalizationHandler)
228 NS_INTERFACE_MAP_ENTRY(nsISupports)
229 NS_INTERFACE_MAP_END
231 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutationFinalizationHandler)
232 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutationFinalizationHandler)
234 void L10nMutations::FlushPendingTranslationsBeforeLoad() {
235 MOZ_ASSERT(mPendingBlockingLoadFlush);
236 mPendingBlockingLoadFlush = false;
237 FlushPendingTranslations();
240 void L10nMutations::FlushPendingTranslations() {
241 if (!mDOMLocalization) {
242 return;
245 nsTArray<OwningNonNull<Element>> elements;
246 for (auto& elem : mPendingElements) {
247 if (elem->HasAttr(nsGkAtoms::datal10nid)) {
248 elements.AppendElement(*elem);
252 mPendingElementsHash.Clear();
253 mPendingElements.Clear();
255 RefPtr<Promise> promise =
256 mDOMLocalization->TranslateElements(elements, IgnoreErrors());
257 if (promise && promise->State() == Promise::PromiseState::Pending) {
258 mPendingPromises++;
259 auto l10nMutationFinalizationHandler =
260 MakeRefPtr<L10nMutationFinalizationHandler>(
261 this, mDOMLocalization->GetParentObject());
262 promise->AppendNativeHandler(l10nMutationFinalizationHandler);
265 MaybeFirePendingTranslationsFinished();
268 void L10nMutations::PendingPromiseSettled() {
269 MOZ_DIAGNOSTIC_ASSERT(mPendingPromises);
270 mPendingPromises--;
271 MaybeFirePendingTranslationsFinished();
274 void L10nMutations::MaybeFirePendingTranslationsFinished() {
275 if (HasPendingMutations()) {
276 return;
279 RefPtr doc = GetDocument();
280 if (NS_WARN_IF(!doc)) {
281 return;
284 if (mBlockingLoad) {
285 mBlockingLoad = false;
286 doc->UnblockOnload(false);
288 nsContentUtils::DispatchEventOnlyToChrome(
289 doc, doc, u"L10nMutationsFinished"_ns, CanBubble::eNo, Cancelable::eNo,
290 Composed::eNo, nullptr);
293 void L10nMutations::Disconnect() {
294 StopRefreshObserver();
295 mDOMLocalization = nullptr;
298 Document* L10nMutations::GetDocument() const {
299 if (!mDOMLocalization) {
300 return nullptr;
302 auto* innerWindow = mDOMLocalization->GetParentObject()->GetAsInnerWindow();
303 if (!innerWindow) {
304 return nullptr;
306 return innerWindow->GetExtantDoc();
309 void L10nMutations::StartRefreshObserver() {
310 if (!mDOMLocalization || mRefreshDriver) {
311 return;
313 if (Document* doc = GetDocument()) {
314 if (nsPresContext* ctx = doc->GetPresContext()) {
315 mRefreshDriver = ctx->RefreshDriver();
319 // If we can't start the refresh driver, it means
320 // that the presContext is not available yet.
321 // In that case, we'll trigger the flush of pending
322 // elements in Document::CreatePresShell.
323 if (mRefreshDriver) {
324 mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
325 "L10n mutations");
326 } else {
327 NS_WARNING("[l10n][mutations] Failed to start a refresh observer.");
331 void L10nMutations::StopRefreshObserver() {
332 if (mRefreshDriver) {
333 mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
334 mRefreshDriver = nullptr;
338 void L10nMutations::OnCreatePresShell() {
339 StopRefreshObserver();
340 if (!mPendingElements.IsEmpty()) {
341 StartRefreshObserver();
345 bool L10nMutations::IsInRoots(nsINode* aNode) {
346 // If the root of the mutated element is in the light DOM,
347 // we know it must be covered by our observer directly.
349 // Otherwise, we need to check if its subtree root is the same
350 // as any of the `DOMLocalization::mRoots` subtree roots.
351 nsINode* root = aNode->SubtreeRoot();
353 // If element is in light DOM, it must be covered by one of
354 // the DOMLocalization roots to end up here.
355 MOZ_ASSERT_IF(!root->IsShadowRoot(),
356 mDOMLocalization->SubtreeRootInRoots(root));
358 return !root->IsShadowRoot() || mDOMLocalization->SubtreeRootInRoots(root);