mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 16:22:00 +00:00
d29206eeb7
This is the actual bug fix. If we were an observer of a detached refresh driver, we won't observe the new one. Depends on D190574 Differential Revision: https://phabricator.services.mozilla.com/D190575
360 lines
10 KiB
C++
360 lines
10 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "L10nMutations.h"
|
|
#include "mozilla/dom/DocumentInlines.h"
|
|
#include "nsRefreshDriver.h"
|
|
#include "DOMLocalization.h"
|
|
#include "mozilla/intl/Localization.h"
|
|
#include "nsThreadManager.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::intl;
|
|
using namespace mozilla::dom;
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(L10nMutations)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElements)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElementsHash)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(L10nMutations)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElements)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElementsHash)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutations)
|
|
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations)
|
|
|
|
L10nMutations::L10nMutations(DOMLocalization* aDOMLocalization)
|
|
: mDOMLocalization(aDOMLocalization) {
|
|
mObserving = true;
|
|
}
|
|
|
|
L10nMutations::~L10nMutations() {
|
|
StopRefreshObserver();
|
|
MOZ_ASSERT(!mDOMLocalization,
|
|
"DOMLocalization<-->L10nMutations cycle should be broken.");
|
|
}
|
|
|
|
void L10nMutations::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
|
|
nsAtom* aAttribute, int32_t aModType,
|
|
const nsAttrValue* aOldValue) {
|
|
if (!mObserving) {
|
|
return;
|
|
}
|
|
|
|
if (aNameSpaceID == kNameSpaceID_None &&
|
|
(aAttribute == nsGkAtoms::datal10nid ||
|
|
aAttribute == nsGkAtoms::datal10nargs)) {
|
|
if (IsInRoots(aElement)) {
|
|
L10nElementChanged(aElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
void L10nMutations::ContentAppended(nsIContent* aChild) {
|
|
if (!mObserving) {
|
|
return;
|
|
}
|
|
|
|
if (!IsInRoots(aChild)) {
|
|
return;
|
|
}
|
|
|
|
Sequence<OwningNonNull<Element>> elements;
|
|
for (nsIContent* node = aChild; node; node = node->GetNextSibling()) {
|
|
if (node->IsElement()) {
|
|
DOMLocalization::GetTranslatables(*node, elements, IgnoreErrors());
|
|
}
|
|
}
|
|
|
|
for (auto& elem : elements) {
|
|
L10nElementChanged(elem);
|
|
}
|
|
}
|
|
|
|
void L10nMutations::ContentInserted(nsIContent* aChild) {
|
|
if (!mObserving) {
|
|
return;
|
|
}
|
|
|
|
if (!aChild->IsElement()) {
|
|
return;
|
|
}
|
|
Element* elem = aChild->AsElement();
|
|
|
|
if (!IsInRoots(elem)) {
|
|
return;
|
|
}
|
|
|
|
Sequence<OwningNonNull<Element>> elements;
|
|
DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors());
|
|
|
|
for (auto& elem : elements) {
|
|
L10nElementChanged(elem);
|
|
}
|
|
}
|
|
|
|
void L10nMutations::ContentRemoved(nsIContent* aChild,
|
|
nsIContent* aPreviousSibling) {
|
|
if (!mObserving || mPendingElements.IsEmpty()) {
|
|
return;
|
|
}
|
|
|
|
Element* elem = Element::FromNode(*aChild);
|
|
if (!elem || !IsInRoots(elem)) {
|
|
return;
|
|
}
|
|
|
|
Sequence<OwningNonNull<Element>> elements;
|
|
DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors());
|
|
|
|
for (auto& elem : elements) {
|
|
if (mPendingElementsHash.EnsureRemoved(elem)) {
|
|
mPendingElements.RemoveElement(elem);
|
|
}
|
|
}
|
|
|
|
if (!HasPendingMutations()) {
|
|
nsContentUtils::AddScriptRunner(NewRunnableMethod(
|
|
"MaybeFirePendingTranslationsFinished", this,
|
|
&L10nMutations::MaybeFirePendingTranslationsFinished));
|
|
}
|
|
}
|
|
|
|
void L10nMutations::L10nElementChanged(Element* aElement) {
|
|
const bool wasEmpty = mPendingElements.IsEmpty();
|
|
|
|
if (mPendingElementsHash.EnsureInserted(aElement)) {
|
|
mPendingElements.AppendElement(aElement);
|
|
}
|
|
|
|
if (!wasEmpty) {
|
|
return;
|
|
}
|
|
|
|
if (!mRefreshDriver) {
|
|
StartRefreshObserver();
|
|
}
|
|
|
|
if (!mBlockingLoad) {
|
|
Document* doc = GetDocument();
|
|
if (doc && doc->GetReadyStateEnum() != Document::READYSTATE_COMPLETE) {
|
|
doc->BlockOnload();
|
|
mBlockingLoad = true;
|
|
}
|
|
}
|
|
|
|
if (mBlockingLoad && !mPendingBlockingLoadFlush) {
|
|
// We want to make sure we flush translations and don't block the load
|
|
// indefinitely (and, in fact, that we do it rather soon, even if the
|
|
// refresh driver is not ticking yet).
|
|
//
|
|
// In some platforms (mainly Wayland) the load of the main document
|
|
// causes vsync to start running and start ticking the refresh driver,
|
|
// so we can't rely on the refresh driver ticking yet.
|
|
RefPtr<nsIRunnable> task =
|
|
NewRunnableMethod("FlushPendingTranslationsBeforeLoad", this,
|
|
&L10nMutations::FlushPendingTranslationsBeforeLoad);
|
|
nsThreadManager::get().DispatchDirectTaskToCurrentThread(task);
|
|
mPendingBlockingLoadFlush = true;
|
|
}
|
|
}
|
|
|
|
void L10nMutations::PauseObserving() { mObserving = false; }
|
|
|
|
void L10nMutations::ResumeObserving() { mObserving = true; }
|
|
|
|
void L10nMutations::WillRefresh(mozilla::TimeStamp aTime) {
|
|
StopRefreshObserver();
|
|
FlushPendingTranslations();
|
|
}
|
|
|
|
/**
|
|
* The handler for the `TranslateElements` promise used to turn
|
|
* a potential rejection into a console warning.
|
|
**/
|
|
class L10nMutationFinalizationHandler final : public PromiseNativeHandler {
|
|
public:
|
|
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
|
NS_DECL_CYCLE_COLLECTION_CLASS(L10nMutationFinalizationHandler)
|
|
|
|
explicit L10nMutationFinalizationHandler(L10nMutations* aMutations,
|
|
nsIGlobalObject* aGlobal)
|
|
: mMutations(aMutations), mGlobal(aGlobal) {}
|
|
|
|
MOZ_CAN_RUN_SCRIPT void Settled() {
|
|
if (RefPtr mutations = mMutations) {
|
|
mutations->PendingPromiseSettled();
|
|
}
|
|
}
|
|
|
|
MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
ErrorResult& aRv) override {
|
|
Settled();
|
|
}
|
|
|
|
MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
ErrorResult& aRv) override {
|
|
nsTArray<nsCString> errors{
|
|
"[dom/l10n] Errors during l10n mutation frame."_ns,
|
|
};
|
|
MaybeReportErrorsToGecko(errors, IgnoreErrors(), mGlobal);
|
|
Settled();
|
|
}
|
|
|
|
private:
|
|
~L10nMutationFinalizationHandler() = default;
|
|
|
|
RefPtr<L10nMutations> mMutations;
|
|
nsCOMPtr<nsIGlobalObject> mGlobal;
|
|
};
|
|
|
|
NS_IMPL_CYCLE_COLLECTION(L10nMutationFinalizationHandler, mGlobal, mMutations)
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutationFinalizationHandler)
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutationFinalizationHandler)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutationFinalizationHandler)
|
|
|
|
void L10nMutations::FlushPendingTranslationsBeforeLoad() {
|
|
MOZ_ASSERT(mPendingBlockingLoadFlush);
|
|
mPendingBlockingLoadFlush = false;
|
|
FlushPendingTranslations();
|
|
}
|
|
|
|
void L10nMutations::FlushPendingTranslations() {
|
|
if (!mDOMLocalization) {
|
|
return;
|
|
}
|
|
|
|
nsTArray<OwningNonNull<Element>> elements;
|
|
for (auto& elem : mPendingElements) {
|
|
if (elem->HasAttr(nsGkAtoms::datal10nid)) {
|
|
elements.AppendElement(*elem);
|
|
}
|
|
}
|
|
|
|
mPendingElementsHash.Clear();
|
|
mPendingElements.Clear();
|
|
|
|
RefPtr<Promise> promise =
|
|
mDOMLocalization->TranslateElements(elements, IgnoreErrors());
|
|
if (promise && promise->State() == Promise::PromiseState::Pending) {
|
|
mPendingPromises++;
|
|
auto l10nMutationFinalizationHandler =
|
|
MakeRefPtr<L10nMutationFinalizationHandler>(
|
|
this, mDOMLocalization->GetParentObject());
|
|
promise->AppendNativeHandler(l10nMutationFinalizationHandler);
|
|
}
|
|
|
|
MaybeFirePendingTranslationsFinished();
|
|
}
|
|
|
|
void L10nMutations::PendingPromiseSettled() {
|
|
MOZ_DIAGNOSTIC_ASSERT(mPendingPromises);
|
|
mPendingPromises--;
|
|
MaybeFirePendingTranslationsFinished();
|
|
}
|
|
|
|
void L10nMutations::MaybeFirePendingTranslationsFinished() {
|
|
if (HasPendingMutations()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr doc = GetDocument();
|
|
if (NS_WARN_IF(!doc)) {
|
|
return;
|
|
}
|
|
|
|
if (mBlockingLoad) {
|
|
mBlockingLoad = false;
|
|
doc->UnblockOnload(false);
|
|
}
|
|
nsContentUtils::DispatchEventOnlyToChrome(
|
|
doc, doc, u"L10nMutationsFinished"_ns, CanBubble::eNo, Cancelable::eNo,
|
|
Composed::eNo, nullptr);
|
|
}
|
|
|
|
void L10nMutations::Disconnect() {
|
|
StopRefreshObserver();
|
|
mDOMLocalization = nullptr;
|
|
}
|
|
|
|
Document* L10nMutations::GetDocument() const {
|
|
if (!mDOMLocalization) {
|
|
return nullptr;
|
|
}
|
|
auto* innerWindow = mDOMLocalization->GetParentObject()->GetAsInnerWindow();
|
|
if (!innerWindow) {
|
|
return nullptr;
|
|
}
|
|
return innerWindow->GetExtantDoc();
|
|
}
|
|
|
|
void L10nMutations::StartRefreshObserver() {
|
|
if (!mDOMLocalization || mRefreshDriver) {
|
|
return;
|
|
}
|
|
if (Document* doc = GetDocument()) {
|
|
if (nsPresContext* ctx = doc->GetPresContext()) {
|
|
mRefreshDriver = ctx->RefreshDriver();
|
|
}
|
|
}
|
|
|
|
// If we can't start the refresh driver, it means
|
|
// that the presContext is not available yet.
|
|
// In that case, we'll trigger the flush of pending
|
|
// elements in Document::CreatePresShell.
|
|
if (mRefreshDriver) {
|
|
mRefreshDriver->AddRefreshObserver(this, FlushType::Style,
|
|
"L10n mutations");
|
|
} else {
|
|
NS_WARNING("[l10n][mutations] Failed to start a refresh observer.");
|
|
}
|
|
}
|
|
|
|
void L10nMutations::StopRefreshObserver() {
|
|
if (mRefreshDriver) {
|
|
mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
|
|
mRefreshDriver = nullptr;
|
|
}
|
|
}
|
|
|
|
void L10nMutations::OnCreatePresShell() {
|
|
StopRefreshObserver();
|
|
if (!mPendingElements.IsEmpty()) {
|
|
StartRefreshObserver();
|
|
}
|
|
}
|
|
|
|
bool L10nMutations::IsInRoots(nsINode* aNode) {
|
|
// If the root of the mutated element is in the light DOM,
|
|
// we know it must be covered by our observer directly.
|
|
//
|
|
// Otherwise, we need to check if its subtree root is the same
|
|
// as any of the `DOMLocalization::mRoots` subtree roots.
|
|
nsINode* root = aNode->SubtreeRoot();
|
|
|
|
// If element is in light DOM, it must be covered by one of
|
|
// the DOMLocalization roots to end up here.
|
|
MOZ_ASSERT_IF(!root->IsShadowRoot(),
|
|
mDOMLocalization->SubtreeRootInRoots(root));
|
|
|
|
return !root->IsShadowRoot() || mDOMLocalization->SubtreeRootInRoots(root);
|
|
}
|