/* -*- 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 "mozilla/dom/UndoManager.h" #include "mozilla/dom/DOMTransactionBinding.h" #include "mozilla/dom/Event.h" #include "nsDOMClassInfoID.h" #include "nsIClassInfo.h" #include "nsIDOMDocument.h" #include "nsIXPCScriptable.h" #include "nsIVariant.h" #include "nsVariant.h" #include "nsINode.h" #include "mozilla/dom/DOMTransactionEvent.h" #include "mozilla/dom/ToJSValue.h" #include "nsContentUtils.h" #include "jsapi.h" #include "nsIDocument.h" #include "mozilla/ErrorResult.h" #include "mozilla/EventDispatcher.h" #include "mozilla/Preferences.h" // Includes for mutation observer. #include "nsIDOMHTMLElement.h" #include "nsStubMutationObserver.h" #include "nsAutoPtr.h" #include "nsTransactionManager.h" // Includes for attribute changed transaction. #include "nsITransaction.h" #include "nsIContent.h" #include "nsIDOMMutationEvent.h" #include "mozilla/dom/Element.h" // Includes for text content changed. #include "nsTextFragment.h" using namespace mozilla; using namespace mozilla::dom; ///////////////////////////////////////////////// // UndoTxn ///////////////////////////////////////////////// /** * A base class to implement methods that behave the same for all * UndoManager transactions. */ class UndoTxn : public nsITransaction { NS_DECL_NSITRANSACTION protected: virtual ~UndoTxn() {} }; NS_IMETHODIMP UndoTxn::DoTransaction() { // Do not do anything the first time we apply this transaction, // changes should already have been applied. return NS_OK; } NS_IMETHODIMP UndoTxn::RedoTransaction() { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP UndoTxn::UndoTransaction() { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP UndoTxn::GetIsTransient(bool* aIsTransient) { *aIsTransient = false; return NS_OK; } NS_IMETHODIMP UndoTxn::Merge(nsITransaction* aTransaction, bool* aResult) { *aResult = false; return NS_OK; } ///////////////////////////////////////////////// // UndoAttrChanged ///////////////////////////////////////////////// /** * Transaction to handle an attribute change to a nsIContent. */ class UndoAttrChanged : public UndoTxn { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(UndoAttrChanged) NS_IMETHOD RedoTransaction() override; NS_IMETHOD UndoTransaction() override; nsresult Init(); UndoAttrChanged(mozilla::dom::Element* aElement, int32_t aNameSpaceID, nsIAtom* aAttribute, int32_t aModType); protected: ~UndoAttrChanged() {} nsresult SaveRedoState(); nsCOMPtr mElement; int32_t mNameSpaceID; nsCOMPtr mAttrAtom; int32_t mModType; nsString mRedoValue; nsString mUndoValue; }; NS_IMPL_CYCLE_COLLECTION(UndoAttrChanged, mElement, mAttrAtom) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UndoAttrChanged) NS_INTERFACE_MAP_ENTRY(nsITransaction) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(UndoAttrChanged) NS_IMPL_CYCLE_COLLECTING_RELEASE(UndoAttrChanged) UndoAttrChanged::UndoAttrChanged(mozilla::dom::Element* aElement, int32_t aNameSpaceID, nsIAtom* aAttribute, int32_t aModType) : mElement(aElement), mNameSpaceID(aNameSpaceID), mAttrAtom(aAttribute), mModType(aModType) {} nsresult UndoAttrChanged::SaveRedoState() { mElement->GetAttr(mNameSpaceID, mAttrAtom, mRedoValue); return NS_OK; } nsresult UndoAttrChanged::Init() { mElement->GetAttr(mNameSpaceID, mAttrAtom, mUndoValue); return NS_OK; } NS_IMETHODIMP UndoAttrChanged::UndoTransaction() { nsresult rv = SaveRedoState(); NS_ENSURE_SUCCESS(rv, rv); switch (mModType) { case nsIDOMMutationEvent::MODIFICATION: mElement->SetAttr(mNameSpaceID, mAttrAtom, mUndoValue, true); return NS_OK; case nsIDOMMutationEvent::ADDITION: mElement->UnsetAttr(mNameSpaceID, mAttrAtom, true); return NS_OK; case nsIDOMMutationEvent::REMOVAL: if (!mElement->HasAttr(mNameSpaceID, mAttrAtom)) { mElement->SetAttr(mNameSpaceID, mAttrAtom, mUndoValue, true); } return NS_OK; } return NS_ERROR_UNEXPECTED; } NS_IMETHODIMP UndoAttrChanged::RedoTransaction() { switch (mModType) { case nsIDOMMutationEvent::MODIFICATION: mElement->SetAttr(mNameSpaceID, mAttrAtom, mRedoValue, true); return NS_OK; case nsIDOMMutationEvent::ADDITION: if (!mElement->HasAttr(mNameSpaceID, mAttrAtom)) { mElement->SetAttr(mNameSpaceID, mAttrAtom, mRedoValue, true); } return NS_OK; case nsIDOMMutationEvent::REMOVAL: mElement->UnsetAttr(mNameSpaceID, mAttrAtom, true); return NS_OK; } return NS_ERROR_UNEXPECTED; } ///////////////////////////////////////////////// // UndoTextChanged ///////////////////////////////////////////////// struct UndoCharacterChangedData { bool mAppend; uint32_t mChangeStart; uint32_t mChangeEnd; uint32_t mReplaceLength; explicit UndoCharacterChangedData(CharacterDataChangeInfo* aChange) : mAppend(aChange->mAppend), mChangeStart(aChange->mChangeStart), mChangeEnd(aChange->mChangeEnd), mReplaceLength(aChange->mReplaceLength) {} }; /** * Transaction to handle a text change to a nsIContent. */ class UndoTextChanged : public UndoTxn { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(UndoTextChanged) NS_IMETHOD RedoTransaction() override; NS_IMETHOD UndoTransaction() override; UndoTextChanged(nsIContent* aContent, CharacterDataChangeInfo* aChange); protected: ~UndoTextChanged() {} void SaveRedoState(); nsCOMPtr mContent; UndoCharacterChangedData mChange; nsString mRedoValue; nsString mUndoValue; }; NS_IMPL_CYCLE_COLLECTION(UndoTextChanged, mContent) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UndoTextChanged) NS_INTERFACE_MAP_ENTRY(nsITransaction) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(UndoTextChanged) NS_IMPL_CYCLE_COLLECTING_RELEASE(UndoTextChanged) UndoTextChanged::UndoTextChanged(nsIContent* aContent, CharacterDataChangeInfo* aChange) : mContent(aContent), mChange(aChange) { const nsTextFragment* text = mContent->GetText(); int32_t numReplaced = mChange.mChangeEnd - mChange.mChangeStart; text->AppendTo(mUndoValue, mChange.mChangeStart, numReplaced); } nsresult UndoTextChanged::RedoTransaction() { nsAutoString text; mContent->AppendTextTo(text); if (text.Length() < mChange.mChangeStart) { return NS_OK; } if (mChange.mAppend) { // Text length should match the change start unless there was a // mutation exterior to the UndoManager in which case we do nothing. if (text.Length() == mChange.mChangeStart) { mContent->AppendText(mRedoValue.get(), mRedoValue.Length(), true); } } else { int32_t numReplaced = mChange.mChangeEnd - mChange.mChangeStart; // The length of the text should be at least as long as the replacement // offset + replaced length, otherwise there was an external mutation. if (mChange.mChangeStart + numReplaced <= text.Length()) { text.Replace(mChange.mChangeStart, numReplaced, mRedoValue); mContent->SetText(text, true); } } return NS_OK; } nsresult UndoTextChanged::UndoTransaction() { SaveRedoState(); nsAutoString text; mContent->AppendTextTo(text); if (text.Length() < mChange.mChangeStart) { return NS_OK; } if (mChange.mAppend) { // The text should at least as long as the redo value in the case // of an append, otherwise there was an external mutation. if (mRedoValue.Length() <= text.Length()) { text.Truncate(text.Length() - mRedoValue.Length()); } } else { // The length of the text should be at least as long as the replacement // offset + replacement length, otherwise there was an external mutation. if (mChange.mChangeStart + mChange.mReplaceLength <= text.Length()) { text.Replace(mChange.mChangeStart, mChange.mReplaceLength, mUndoValue); } } mContent->SetText(text, true); return NS_OK; } void UndoTextChanged::SaveRedoState() { const nsTextFragment* text = mContent->GetText(); mRedoValue.Truncate(); // The length of the text should be at least as long as the replacement // offset + replacement length, otherwise there was an external mutation. if (mChange.mChangeStart + mChange.mReplaceLength <= text->GetLength()) { text->AppendTo(mRedoValue, mChange.mChangeStart, mChange.mReplaceLength); } } ///////////////////////////////////////////////// // UndoContentAppend ///////////////////////////////////////////////// /** * Transaction to handle appending content to a nsIContent. */ class UndoContentAppend : public UndoTxn { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(UndoContentAppend) nsresult Init(int32_t aFirstIndex); NS_IMETHOD RedoTransaction() override; NS_IMETHOD UndoTransaction() override; explicit UndoContentAppend(nsIContent* aContent); protected: ~UndoContentAppend() {} nsCOMPtr mContent; nsCOMArray mChildren; }; NS_IMPL_CYCLE_COLLECTION(UndoContentAppend, mContent, mChildren) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UndoContentAppend) NS_INTERFACE_MAP_ENTRY(nsITransaction) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(UndoContentAppend) NS_IMPL_CYCLE_COLLECTING_RELEASE(UndoContentAppend) UndoContentAppend::UndoContentAppend(nsIContent* aContent) { mContent = aContent; } nsresult UndoContentAppend::Init(int32_t aFirstIndex) { for (uint32_t i = aFirstIndex; i < mContent->GetChildCount(); i++) { NS_ENSURE_TRUE(mChildren.AppendObject(mContent->GetChildAt(i)), NS_ERROR_OUT_OF_MEMORY); } return NS_OK; } nsresult UndoContentAppend::RedoTransaction() { for (int32_t i = 0; i < mChildren.Count(); i++) { if (!mChildren[i]->GetParentNode()) { mContent->AppendChildTo(mChildren[i], true); } } return NS_OK; } nsresult UndoContentAppend::UndoTransaction() { for (int32_t i = mChildren.Count() - 1; i >= 0; i--) { if (mChildren[i]->GetParentNode() == mContent) { IgnoredErrorResult error; mContent->RemoveChild(*mChildren[i], error); } } return NS_OK; } ///////////////////////////////////////////////// // UndoContentInsert ///////////////////////////////////////////////// /** * Transaction to handle inserting content into a nsIContent. */ class UndoContentInsert : public UndoTxn { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(UndoContentInsert) NS_IMETHOD UndoTransaction() override; NS_IMETHOD RedoTransaction() override; UndoContentInsert(nsIContent* aContent, nsIContent* aChild, int32_t aInsertIndex); protected: ~UndoContentInsert() {} nsCOMPtr mContent; nsCOMPtr mChild; nsCOMPtr mNextNode; }; NS_IMPL_CYCLE_COLLECTION(UndoContentInsert, mContent, mChild, mNextNode) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UndoContentInsert) NS_INTERFACE_MAP_ENTRY(nsITransaction) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(UndoContentInsert) NS_IMPL_CYCLE_COLLECTING_RELEASE(UndoContentInsert) UndoContentInsert::UndoContentInsert(nsIContent* aContent, nsIContent* aChild, int32_t aInsertIndex) : mContent(aContent), mChild(aChild) { mNextNode = mContent->GetChildAt(aInsertIndex + 1); } nsresult UndoContentInsert::RedoTransaction() { if (!mChild) { return NS_ERROR_UNEXPECTED; } // Check if node already has parent. if (mChild->GetParentNode()) { return NS_OK; } // Check to see if next sibling has same parent. if (mNextNode && mNextNode->GetParentNode() != mContent) { return NS_OK; } IgnoredErrorResult error; nsCOMPtr refNode = mNextNode; mContent->InsertBefore(*mChild, refNode, error); return NS_OK; } nsresult UndoContentInsert::UndoTransaction() { if (!mChild) { return NS_ERROR_UNEXPECTED; } // Check if the parent is the same. if (mChild->GetParentNode() != mContent) { return NS_OK; } // Check of the parent of the next node is the same. if (mNextNode && mNextNode->GetParentNode() != mContent) { return NS_OK; } // Check that the next node has not changed. if (mChild->GetNextSibling() != mNextNode) { return NS_OK; } IgnoredErrorResult error; mContent->RemoveChild(*mChild, error); return NS_OK; } ///////////////////////////////////////////////// // UndoContentRemove ///////////////////////////////////////////////// /** * Transaction to handle removing content from an nsIContent. */ class UndoContentRemove : public UndoTxn { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(UndoContentRemove) NS_IMETHOD UndoTransaction() override; NS_IMETHOD RedoTransaction() override; nsresult Init(int32_t aInsertIndex); UndoContentRemove(nsIContent* aContent, nsIContent* aChild, int32_t aInsertIndex); protected: ~UndoContentRemove() {} nsCOMPtr mContent; nsCOMPtr mChild; nsCOMPtr mNextNode; }; NS_IMPL_CYCLE_COLLECTION(UndoContentRemove, mContent, mChild, mNextNode) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UndoContentRemove) NS_INTERFACE_MAP_ENTRY(nsITransaction) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(UndoContentRemove) NS_IMPL_CYCLE_COLLECTING_RELEASE(UndoContentRemove) nsresult UndoContentRemove::Init(int32_t aInsertIndex) { return NS_OK; } UndoContentRemove::UndoContentRemove(nsIContent* aContent, nsIContent* aChild, int32_t aInsertIndex) : mContent(aContent), mChild(aChild) { mNextNode = mContent->GetChildAt(aInsertIndex); } nsresult UndoContentRemove::UndoTransaction() { if (!mChild) { return NS_ERROR_UNEXPECTED; } // Check if child has a parent. if (mChild->GetParentNode()) { return NS_OK; } // Make sure next sibling is still under same parent. if (mNextNode && mNextNode->GetParentNode() != mContent) { return NS_OK; } IgnoredErrorResult error; nsCOMPtr refNode = mNextNode; mContent->InsertBefore(*mChild, refNode, error); return NS_OK; } nsresult UndoContentRemove::RedoTransaction() { if (!mChild) { return NS_ERROR_UNEXPECTED; } // Check that the parent has not changed. if (mChild->GetParentNode() != mContent) { return NS_OK; } // Check that the next node still has the same parent. if (mNextNode && mNextNode->GetParentNode() != mContent) { return NS_OK; } // Check that the next sibling has not changed. if (mChild->GetNextSibling() != mNextNode) { return NS_OK; } IgnoredErrorResult error; mContent->RemoveChild(*mChild, error); return NS_OK; } ///////////////////////////////////////////////// // UndoMutationObserver ///////////////////////////////////////////////// /** * Watches for DOM mutations in a particular element and its * descendants to create transactions that undo and redo * the mutations. Each UndoMutationObserver corresponds * to an undo scope. */ class UndoMutationObserver : public nsStubMutationObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTEWILLCHANGE NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED explicit UndoMutationObserver(nsITransactionManager* aTxnManager); protected: /** * Checks if |aContent| is within the undo scope of this * UndoMutationObserver. */ bool IsManagerForMutation(nsIContent* aContent); virtual ~UndoMutationObserver() {} nsITransactionManager* mTxnManager; // [RawPtr] UndoManager holds strong // reference. }; NS_IMPL_ISUPPORTS(UndoMutationObserver, nsIMutationObserver) bool UndoMutationObserver::IsManagerForMutation(nsIContent* aContent) { nsCOMPtr currentNode = aContent; RefPtr undoManager; // Get the UndoManager of nearest ancestor with an UndoManager. while (currentNode && !undoManager) { nsCOMPtr htmlElem = do_QueryInterface(currentNode); if (htmlElem) { undoManager = htmlElem->GetUndoManager(); } currentNode = currentNode->GetParentNode(); } if (!undoManager) { // Check against document UndoManager if we were unable to find an // UndoManager in an ancestor element. nsIDocument* doc = aContent->OwnerDoc(); NS_ENSURE_TRUE(doc, false); undoManager = doc->GetUndoManager(); // The document will not have an undoManager if the // documentElement is removed. NS_ENSURE_TRUE(undoManager, false); } // Check if the nsITransactionManager is the same for both the // mutation observer and the nsIContent. return undoManager->GetTransactionManager() == mTxnManager; } UndoMutationObserver::UndoMutationObserver(nsITransactionManager* aTxnManager) : mTxnManager(aTxnManager) {} void UndoMutationObserver::AttributeWillChange(nsIDocument* aDocument, mozilla::dom::Element* aElement, int32_t aNameSpaceID, nsIAtom* aAttribute, int32_t aModType, const nsAttrValue* aNewValue) { if (!IsManagerForMutation(aElement)) { return; } RefPtr undoTxn = new UndoAttrChanged(aElement, aNameSpaceID, aAttribute, aModType); if (NS_SUCCEEDED(undoTxn->Init())) { mTxnManager->DoTransaction(undoTxn); } } void UndoMutationObserver::CharacterDataWillChange(nsIDocument* aDocument, nsIContent* aContent, CharacterDataChangeInfo* aInfo) { if (!IsManagerForMutation(aContent)) { return; } RefPtr undoTxn = new UndoTextChanged(aContent, aInfo); mTxnManager->DoTransaction(undoTxn); } void UndoMutationObserver::ContentAppended(nsIDocument* aDocument, nsIContent* aContainer, nsIContent* aFirstNewContent, int32_t aNewIndexInContainer) { if (!IsManagerForMutation(aContainer)) { return; } RefPtr txn = new UndoContentAppend(aContainer); if (NS_SUCCEEDED(txn->Init(aNewIndexInContainer))) { mTxnManager->DoTransaction(txn); } } void UndoMutationObserver::ContentInserted(nsIDocument* aDocument, nsIContent* aContainer, nsIContent* aChild, int32_t aIndexInContainer) { if (!IsManagerForMutation(aContainer)) { return; } RefPtr txn = new UndoContentInsert(aContainer, aChild, aIndexInContainer); mTxnManager->DoTransaction(txn); } void UndoMutationObserver::ContentRemoved(nsIDocument *aDocument, nsIContent* aContainer, nsIContent* aChild, int32_t aIndexInContainer, nsIContent* aPreviousSibling) { if (!IsManagerForMutation(aContainer)) { return; } RefPtr txn = new UndoContentRemove(aContainer, aChild, aIndexInContainer); mTxnManager->DoTransaction(txn); } ///////////////////////////////////////////////// // FunctionCallTxn ///////////////////////////////////////////////// /** * A transaction that calls members on the transaction * object. */ class FunctionCallTxn : public UndoTxn { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(FunctionCallTxn) // Flags static const uint32_t CALL_ON_REDO = 1; static const uint32_t CALL_ON_UNDO = 2; NS_IMETHOD RedoTransaction() override; NS_IMETHOD UndoTransaction() override; FunctionCallTxn(DOMTransaction* aTransaction, uint32_t aFlags); protected: ~FunctionCallTxn() {} /** * Call a function member on the transaction object with the * specified function name. */ RefPtr mTransaction; uint32_t mFlags; }; NS_IMPL_CYCLE_COLLECTION(FunctionCallTxn, mTransaction) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FunctionCallTxn) NS_INTERFACE_MAP_ENTRY(nsITransaction) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(FunctionCallTxn) NS_IMPL_CYCLE_COLLECTING_RELEASE(FunctionCallTxn) FunctionCallTxn::FunctionCallTxn(DOMTransaction* aTransaction, uint32_t aFlags) : mTransaction(aTransaction), mFlags(aFlags) {} nsresult FunctionCallTxn::RedoTransaction() { if (!(mFlags & CALL_ON_REDO)) { return NS_OK; } // We ignore rv because we want to avoid the rollback behavior of the // nsITransactionManager. IgnoredErrorResult rv; RefPtr redo = mTransaction->GetRedo(rv); if (!rv.Failed() && redo) { redo->Call(mTransaction.get(), rv); } return NS_OK; } nsresult FunctionCallTxn::UndoTransaction() { if (!(mFlags & CALL_ON_UNDO)) { return NS_OK; } // We ignore rv because we want to avoid the rollback behavior of the // nsITransactionManager. IgnoredErrorResult rv; RefPtr undo = mTransaction->GetUndo(rv); if (!rv.Failed() && undo) { undo->Call(mTransaction.get(), rv); } return NS_OK; } ///////////////////////////////////////////////// // TxnScopeGuard ///////////////////////////////////////////////// namespace mozilla { namespace dom { class TxnScopeGuard { public: explicit TxnScopeGuard(UndoManager* aUndoManager) : mUndoManager(aUndoManager) { mUndoManager->mInTransaction = true; } ~TxnScopeGuard() { mUndoManager->mInTransaction = false; } protected: UndoManager* mUndoManager; }; } // namespace dom } // namespace mozilla ///////////////////////////////////////////////// // UndoManager ///////////////////////////////////////////////// NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(UndoManager, mTxnManager, mHostNode) NS_IMPL_CYCLE_COLLECTING_ADDREF(UndoManager) NS_IMPL_CYCLE_COLLECTING_RELEASE(UndoManager) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UndoManager) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END UndoManager::UndoManager(nsIContent* aNode) : mHostNode(aNode), mInTransaction(false), mIsDisconnected(false) { mTxnManager = new nsTransactionManager(); } UndoManager::~UndoManager() {} void UndoManager::Transact(JSContext* aCx, DOMTransaction& aTransaction, bool aMerge, ErrorResult& aRv) { if (mIsDisconnected || mInTransaction) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } TxnScopeGuard guard(this); // First try executing an automatic transaction. RefPtr executeAutomatic = aTransaction.GetExecuteAutomatic(aRv); if (aRv.Failed()) { return; } if (executeAutomatic) { AutomaticTransact(&aTransaction, executeAutomatic, aRv); } else { ManualTransact(&aTransaction, aRv); } if (aRv.Failed()) { return; } if (aMerge) { nsresult rv = mTxnManager->BatchTopUndo(); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } } DispatchTransactionEvent(aCx, NS_LITERAL_STRING("DOMTransaction"), 0, aRv); if (aRv.Failed()) { return; } } void UndoManager::AutomaticTransact(DOMTransaction* aTransaction, DOMTransactionCallback* aCallback, ErrorResult& aRv) { MOZ_ASSERT(aCallback); nsCOMPtr mutationObserver = new UndoMutationObserver(mTxnManager); // Transaction to call the "undo" method after the automatic transaction // has been undone. RefPtr undoTxn = new FunctionCallTxn(aTransaction, FunctionCallTxn::CALL_ON_UNDO); // Transaction to call the "redo" method after the automatic transaction // has been redone. RefPtr redoTxn = new FunctionCallTxn(aTransaction, FunctionCallTxn::CALL_ON_REDO); mTxnManager->BeginBatch(aTransaction); mTxnManager->DoTransaction(undoTxn); mHostNode->AddMutationObserver(mutationObserver); aCallback->Call(aTransaction, aRv); mHostNode->RemoveMutationObserver(mutationObserver); mTxnManager->DoTransaction(redoTxn); mTxnManager->EndBatch(true); if (aRv.Failed()) { mTxnManager->RemoveTopUndo(); } } void UndoManager::ManualTransact(DOMTransaction* aTransaction, ErrorResult& aRv) { RefPtr txn = new FunctionCallTxn(aTransaction, FunctionCallTxn::CALL_ON_REDO | FunctionCallTxn::CALL_ON_UNDO); RefPtr execute = aTransaction->GetExecute(aRv); if (!aRv.Failed() && execute) { execute->Call(aTransaction, aRv); } if (aRv.Failed()) { return; } mTxnManager->BeginBatch(aTransaction); mTxnManager->DoTransaction(txn); mTxnManager->EndBatch(true); } uint32_t UndoManager::GetPosition(ErrorResult& aRv) { int32_t numRedo; nsresult rv = mTxnManager->GetNumberOfRedoItems(&numRedo); if (NS_FAILED(rv)) { aRv.Throw(rv); return 0; } MOZ_ASSERT(numRedo >= 0, "Number of redo items should not be negative"); return numRedo; } uint32_t UndoManager::GetLength(ErrorResult& aRv) { int32_t numRedo; nsresult rv = mTxnManager->GetNumberOfRedoItems(&numRedo); if (NS_FAILED(rv)) { aRv.Throw(rv); return 0; } int32_t numUndo; rv = mTxnManager->GetNumberOfUndoItems(&numUndo); if (NS_FAILED(rv)) { aRv.Throw(rv); return 0; } return numRedo + numUndo; } void UndoManager::ItemInternal(uint32_t aIndex, nsTArray& aItems, ErrorResult& aRv) { int32_t numRedo; nsresult rv = mTxnManager->GetNumberOfRedoItems(&numRedo); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } MOZ_ASSERT(numRedo >= 0, "Number of redo items should not be negative"); int32_t numUndo; rv = mTxnManager->GetNumberOfUndoItems(&numUndo); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } MOZ_ASSERT(numUndo >= 0, "Number of undo items should not be negative"); MOZ_ASSERT(aIndex < (uint32_t) numRedo + numUndo, "Index should be within bounds."); nsCOMPtr txnList; int32_t listIndex = aIndex; if (aIndex < (uint32_t) numRedo) { // Index is an redo. mTxnManager->GetRedoList(getter_AddRefs(txnList)); } else { // Index is a undo. mTxnManager->GetUndoList(getter_AddRefs(txnList)); // We need to adjust the index because the undo list indices will // be in the reverse order. listIndex = numRedo + numUndo - aIndex - 1; } // Obtain data from transaction list and convert to list of // DOMTransaction*. nsISupports** listData; uint32_t listDataLength; rv = txnList->GetData(listIndex, &listDataLength, &listData); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } for (uint32_t i = 0; i < listDataLength; i++) { aItems.AppendElement(static_cast(listData[i])); NS_RELEASE(listData[i]); } free(listData); } void UndoManager::Item(uint32_t aIndex, Nullable > >& aItems, ErrorResult& aRv) { int32_t numRedo; nsresult rv = mTxnManager->GetNumberOfRedoItems(&numRedo); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } MOZ_ASSERT(numRedo >= 0, "Number of redo items should not be negative"); int32_t numUndo; rv = mTxnManager->GetNumberOfUndoItems(&numUndo); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } MOZ_ASSERT(numUndo >= 0, "Number of undo items should not be negative"); if (aIndex >= (uint32_t) numRedo + numUndo) { // If the index is out of bounds, then return null. aItems.SetNull(); return; } nsTArray transactions; ItemInternal(aIndex, transactions, aRv); if (aRv.Failed()) { return; } nsTArray >& items = aItems.SetValue(); for (uint32_t i = 0; i < transactions.Length(); i++) { items.AppendElement(transactions[i]); } } void UndoManager::Undo(JSContext* aCx, ErrorResult& aRv) { if (mIsDisconnected || mInTransaction) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } uint32_t position = GetPosition(aRv); if (aRv.Failed()) { return; } uint32_t length = GetLength(aRv); if (aRv.Failed()) { return; } // Stop if there are no transactions left to undo. if (position >= length) { return; } TxnScopeGuard guard(this); nsresult rv = mTxnManager->UndoTransaction(); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } DispatchTransactionEvent(aCx, NS_LITERAL_STRING("undo"), position, aRv); if (aRv.Failed()) { return; } } void UndoManager::Redo(JSContext* aCx, ErrorResult& aRv) { if (mIsDisconnected || mInTransaction) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } uint32_t position = GetPosition(aRv); if (aRv.Failed()) { return; } // Stop if there are no transactions left to redo. if (position <= 0) { return; } TxnScopeGuard guard(this); nsresult rv = mTxnManager->RedoTransaction(); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } DispatchTransactionEvent(aCx, NS_LITERAL_STRING("redo"), position - 1, aRv); if (aRv.Failed()) { return; } } void UndoManager::DispatchTransactionEvent(JSContext* aCx, const nsAString& aType, uint32_t aPreviousPosition, ErrorResult& aRv) { nsTArray items; ItemInternal(aPreviousPosition, items, aRv); if (aRv.Failed()) { return; } JS::Rooted array(aCx); if (!ToJSValue(aCx, items, &array)) { return; } RootedDictionary init(aCx); init.mBubbles = true; init.mCancelable = false; init.mTransactions = array; RefPtr event = DOMTransactionEvent::Constructor(mHostNode, aType, init); event->SetTrusted(true); EventDispatcher::DispatchDOMEvent(mHostNode, nullptr, event, nullptr, nullptr); } void UndoManager::ClearUndo(ErrorResult& aRv) { if (mIsDisconnected || mInTransaction) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } nsresult rv = mTxnManager->ClearUndoStack(); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } } void UndoManager::ClearRedo(ErrorResult& aRv) { if (mIsDisconnected || mInTransaction) { aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); return; } nsresult rv = mTxnManager->ClearRedoStack(); if (NS_FAILED(rv)) { aRv.Throw(rv); return; } } nsITransactionManager* UndoManager::GetTransactionManager() { return mTxnManager; } void UndoManager::Disconnect() { mIsDisconnected = true; }