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
)
35 NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations
)
36 NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations
)
38 L10nMutations::L10nMutations(DOMLocalization
* aDOMLocalization
)
39 : mDOMLocalization(aDOMLocalization
) {
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
) {
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
) {
70 if (!IsInRoots(aChild
)) {
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
) {
91 if (!aChild
->IsElement()) {
94 Element
* elem
= aChild
->AsElement();
96 if (!IsInRoots(elem
)) {
100 Sequence
<OwningNonNull
<Element
>> elements
;
101 DOMLocalization::GetTranslatables(*aChild
, elements
, IgnoreErrors());
103 for (auto& elem
: elements
) {
104 L10nElementChanged(elem
);
108 void L10nMutations::ContentWillBeRemoved(nsIContent
* aChild
,
109 const BatchRemovalState
*) {
110 if (!mObserving
|| mPendingElements
.IsEmpty()) {
114 Element
* elem
= Element::FromNode(*aChild
);
115 if (!elem
|| !IsInRoots(elem
)) {
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
);
146 if (!mRefreshDriver
) {
147 StartRefreshObserver();
150 if (!mBlockingLoad
) {
151 Document
* doc
= GetDocument();
152 if (doc
&& doc
->GetReadyStateEnum() != Document::READYSTATE_COMPLETE
) {
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
{
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
{
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
);
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
)
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
) {
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
) {
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
);
271 MaybeFirePendingTranslationsFinished();
274 void L10nMutations::MaybeFirePendingTranslationsFinished() {
275 if (HasPendingMutations()) {
279 RefPtr doc
= GetDocument();
280 if (NS_WARN_IF(!doc
)) {
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
) {
302 auto* innerWindow
= mDOMLocalization
->GetParentObject()->GetAsInnerWindow();
306 return innerWindow
->GetExtantDoc();
309 void L10nMutations::StartRefreshObserver() {
310 if (!mDOMLocalization
|| mRefreshDriver
) {
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
,
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
);