/* 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 "MoveNodeTransaction.h" #include "EditorBase.h" // for EditorBase #include "EditorDOMAPIWrapper.h" // for AutoNodeAPIWrapper #include "EditorDOMPoint.h" // for EditorDOMPoint #include "HTMLEditor.h" // for HTMLEditor #include "HTMLEditUtils.h" // for HTMLEditUtils #include "mozilla/Likely.h" #include "mozilla/Logging.h" #include "mozilla/ToString.h" #include "nsDebug.h" // for NS_WARNING, etc. #include "nsError.h" // for NS_ERROR_NULL_POINTER, etc. #include "nsIContent.h" // for nsIContent #include "nsString.h" // for nsString namespace mozilla { using namespace dom; template already_AddRefed MoveSiblingsTransaction::MaybeCreate(HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove, nsIContent& aLastContentToMove, const EditorDOMPoint& aPointToInsert); template already_AddRefed MoveSiblingsTransaction::MaybeCreate(HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove, nsIContent& aLastContentToMove, const EditorRawDOMPoint& aPointToInsert); // static template already_AddRefed MoveSiblingsTransaction::MaybeCreate( HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove, nsIContent& aLastContentToMove, const EditorDOMPointBase& aPointToInsert) { if (NS_WARN_IF(!aFirstContentToMove.GetParentNode()) || NS_WARN_IF(&aFirstContentToMove == &aLastContentToMove) || NS_WARN_IF(aFirstContentToMove.GetParentNode() != aLastContentToMove.GetParentNode()) || NS_WARN_IF(!aPointToInsert.IsSet())) { return nullptr; } // The destination should be editable, but it may be in an orphan node // or sub-tree to reduce number of DOM mutation events. In such case, // we're okay to move a node into the non-editable content because we // can assume that the caller will insert it into an editable element. if (NS_WARN_IF(aPointToInsert.IsInComposedDoc() && !HTMLEditUtils::IsSimplyEditableNode( *aPointToInsert.GetContainer()))) { return nullptr; } const uint32_t numberOfSiblings = [&]() -> uint32_t { uint32_t num = 1; for (nsIContent* content = aFirstContentToMove.GetNextSibling(); content; content = content->GetNextSibling()) { // TODO: We should not allow to move a node to improper container element. // However, this is currently used to move invalid parent while // processing the nodes. Therefore, treating the case as error // breaks a lot. if (NS_WARN_IF(content->IsInComposedDoc() && !HTMLEditUtils::IsRemovableNode(*content))) { return 0; } num++; if (content == &aLastContentToMove) { return num; } } return 0; }(); if (NS_WARN_IF(!numberOfSiblings)) { return nullptr; } RefPtr transaction = new MoveSiblingsTransaction( aHTMLEditor, aFirstContentToMove, aLastContentToMove, numberOfSiblings, aPointToInsert); return transaction.forget(); } template MoveSiblingsTransaction::MoveSiblingsTransaction( HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove, nsIContent& aLastContentToMove, uint32_t aNumberOfSiblings, const EditorDOMPointBase& aPointToInsert) : MoveNodeTransactionBase(aHTMLEditor, aLastContentToMove, aPointToInsert.template To()) { mSiblingsToMove.SetCapacity(aNumberOfSiblings); for (nsIContent* content = &aFirstContentToMove; content; content = content->GetNextSibling()) { mSiblingsToMove.AppendElement(*content); if (content == &aLastContentToMove) { break; } } MOZ_ASSERT(mSiblingsToMove.Length() == aNumberOfSiblings); } std::ostream& operator<<(std::ostream& aStream, const MoveSiblingsTransaction& aTransaction) { auto DumpNodeDetails = [&](const nsINode* aNode) { if (aNode) { if (aNode->IsText()) { nsAutoString data; aNode->AsText()->GetData(data); aStream << " (#text \"" << NS_ConvertUTF16toUTF8(data).get() << "\")"; } else { aStream << " (" << *aNode << ")"; } } }; aStream << "{ mSiblingsToMove[0]=" << aTransaction.mSiblingsToMove[0].get(); DumpNodeDetails(aTransaction.mSiblingsToMove[0]); aStream << ", mSiblingsToMove[" << aTransaction.mSiblingsToMove.Length() - 1 << aTransaction.mSiblingsToMove.LastElement().get(); DumpNodeDetails(aTransaction.mSiblingsToMove.LastElement()); aStream << ", mContainer=" << aTransaction.mContainer.get(); DumpNodeDetails(aTransaction.mContainer); aStream << ", mReference=" << aTransaction.mReference.get(); DumpNodeDetails(aTransaction.mReference); aStream << ", mOldContainer=" << aTransaction.mOldContainer.get(); DumpNodeDetails(aTransaction.mOldContainer); aStream << ", mOldNextSibling=" << aTransaction.mOldNextSibling.get(); DumpNodeDetails(aTransaction.mOldNextSibling); aStream << ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }"; return aStream; } NS_IMPL_CYCLE_COLLECTION_INHERITED(MoveSiblingsTransaction, EditTransactionBase, mSiblingsToMove) NS_IMPL_ADDREF_INHERITED(MoveSiblingsTransaction, EditTransactionBase) NS_IMPL_RELEASE_INHERITED(MoveSiblingsTransaction, EditTransactionBase) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MoveSiblingsTransaction) NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) NS_IMETHODIMP MoveSiblingsTransaction::DoTransaction() { MOZ_LOG(GetLogModule(), LogLevel::Info, ("%p MoveSiblingsTransaction::%s this=%s", this, __FUNCTION__, ToString(*this).c_str())); mDone = true; return DoTransactionInternal(); } nsresult MoveSiblingsTransaction::DoTransactionInternal() { MOZ_DIAGNOSTIC_ASSERT(mHTMLEditor); MOZ_DIAGNOSTIC_ASSERT(!mSiblingsToMove.IsEmpty()); MOZ_DIAGNOSTIC_ASSERT(mContainer); MOZ_DIAGNOSTIC_ASSERT(mOldContainer); { const OwningNonNull htmlEditor = *mHTMLEditor; const OwningNonNull newContainer = *mContainer; const nsCOMPtr newNextSibling = mReference; const CopyableAutoTArray, 64> siblingsToMove( mSiblingsToMove); AutoMoveNodeSelNotify notifier( htmlEditor->RangeUpdaterRef(), mReference ? EditorRawDOMPoint(mReference) : EditorRawDOMPoint::AtEndOf(*newContainer)); // First, remove all nodes from the DOM if they are removable. Then, // IMEContentObserver can use cache to avoid to compute the start offset of // each deleting text. RemoveAllSiblingsToMove(htmlEditor, siblingsToMove, notifier); // Next, insert all removed nodes into the DOM. Then, IMEContentObserver // can use cache to avoid to compute the start offset of each inserting // text. InsertAllSiblingsToMove(htmlEditor, siblingsToMove, newContainer, newNextSibling, notifier); } return NS_WARN_IF(mHTMLEditor->Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; } NS_IMETHODIMP MoveSiblingsTransaction::UndoTransaction() { MOZ_LOG(GetLogModule(), LogLevel::Info, ("%p MoveSiblingsTransaction::%s this=%s", this, __FUNCTION__, ToString(*this).c_str())); mDone = false; if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(mSiblingsToMove.IsEmpty()) || NS_WARN_IF(!IsSiblingsToMoveValid()) || NS_WARN_IF(!mOldContainer)) { // Perhaps, nulled-out by the cycle collector. return NS_ERROR_FAILURE; } // If the original point has been changed, refer mOldNextSibling if it's // reasonable. Otherwise, use end of the old container. if (mOldNextSibling && mOldContainer != mOldNextSibling->GetParentNode()) { // TODO: Check whether the new container is proper one for containing // content in mSiblingsToMove. However, there are few testcases so // that we shouldn't change here without creating a lot of undo tests. if (mOldNextSibling->GetParentNode() && (mOldNextSibling->IsInComposedDoc() || !mOldContainer->IsInComposedDoc())) { mOldContainer = mOldNextSibling->GetParentNode(); } else { mOldNextSibling = nullptr; // end of mOldContainer } } if (MOZ_UNLIKELY(mOldContainer->IsInComposedDoc() && !HTMLEditUtils::IsSimplyEditableNode(*mOldContainer))) { NS_WARNING( "MoveSiblingsTransaction::UndoTransaction() couldn't move the " "content into the old container due to non-editable one"); return NS_ERROR_FAILURE; } // And store the latest node which should be referred at redoing. mContainer = mSiblingsToMove.LastElement()->GetParentNode(); mReference = mSiblingsToMove.LastElement()->GetNextSibling(); { const OwningNonNull htmlEditor = *mHTMLEditor; const OwningNonNull oldContainer = *mOldContainer; const nsCOMPtr oldNextSibling = mOldNextSibling; const CopyableAutoTArray, 64> siblingsToMove( mSiblingsToMove); AutoMoveNodeSelNotify notifier( htmlEditor->RangeUpdaterRef(), oldNextSibling ? EditorRawDOMPoint(oldNextSibling) : EditorRawDOMPoint::AtEndOf(*oldContainer)); // First, remove all nodes from the DOM if they are removable. Then, // IMEContentObserver can use cache to avoid to compute the start offset of // each deleting text. RemoveAllSiblingsToMove(htmlEditor, siblingsToMove, notifier); // Next, insert all removed nodes into the DOM. Then, IMEContentObserver // can use cache to avoid to compute the start offset of each inserting // text. InsertAllSiblingsToMove(htmlEditor, siblingsToMove, oldContainer, oldNextSibling, notifier); } return NS_WARN_IF(mHTMLEditor->Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK; } NS_IMETHODIMP MoveSiblingsTransaction::RedoTransaction() { MOZ_LOG(GetLogModule(), LogLevel::Info, ("%p MoveSiblingsTransaction::%s this=%s", this, __FUNCTION__, ToString(*this).c_str())); mDone = true; if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(mSiblingsToMove.IsEmpty()) || NS_WARN_IF(!IsSiblingsToMoveValid()) || NS_WARN_IF(!mContainer)) { // Perhaps, nulled-out by the cycle collector. return NS_ERROR_FAILURE; } // If the inserting point has been changed, refer mReference if it's // reasonable. Otherwise, use end of the container. if (mReference && mContainer != mReference->GetParentNode()) { // TODO: Check whether the new container is proper one for containing // mContentToMove. However, there are few testcases so that we // shouldn't change here without creating a lot of redo tests. if (mReference->GetParentNode() && (mReference->IsInComposedDoc() || !mContainer->IsInComposedDoc())) { mContainer = mReference->GetParentNode(); } else { mReference = nullptr; // end of mContainer } } if (MOZ_UNLIKELY(mContainer->IsInComposedDoc() && !HTMLEditUtils::IsSimplyEditableNode(*mContainer))) { NS_WARNING( "MoveSiblingsTransaction::RedoTransaction() couldn't move the " "content into the new container due to non-editable one"); return NS_ERROR_FAILURE; } // And store the latest node which should be back. mOldContainer = mSiblingsToMove.LastElement()->GetParentNode(); mOldNextSibling = mSiblingsToMove.LastElement()->GetNextSibling(); nsresult rv = DoTransactionInternal(); if (NS_FAILED(rv)) { NS_WARNING("MoveSiblingsTransaction::DoTransactionInternal() failed"); return rv; } return NS_OK; } nsIContent* MoveSiblingsTransaction::GetFirstMovedContent() const { nsINode* const expectedContainer = mDone ? mContainer : mOldContainer; for (const OwningNonNull& content : mSiblingsToMove) { if (MOZ_LIKELY(content->GetParentNode() == expectedContainer)) { return content; } } return nullptr; } nsIContent* MoveSiblingsTransaction::GetLastMovedContent() const { nsINode* const expectedContainer = mDone ? mContainer : mOldContainer; for (const OwningNonNull& content : Reversed(mSiblingsToMove)) { if (MOZ_LIKELY(content->GetParentNode() == expectedContainer)) { return content; } } return nullptr; } void MoveSiblingsTransaction::RemoveAllSiblingsToMove( HTMLEditor& aHTMLEditor, const nsTArray>& aClonedSiblingsToMove, AutoMoveNodeSelNotify& aNotifier) const { // Be aware, if we're undoing or redoing, some aClonedSiblingsToMove may not // be the adjacent sibling of prev/next element in the array. Therefore, we // may need to compute the index within the expensive path. // First, we need to make AutoMoveNodeSelNotify instances store all indices of // the moving content nodes. { for (const OwningNonNull& contentToMove : aClonedSiblingsToMove) { if (contentToMove->IsInComposedDoc() && !HTMLEditUtils::IsRemovableNode(contentToMove)) { continue; } aNotifier.AppendContentWhichWillBeMoved(contentToMove); } } // Then, remove all nodes unless not removable. for (const size_t i : IntegerRange(aNotifier.MovingContentCount())) { nsIContent* const contentToMove = aNotifier.GetContentAt(i); MOZ_ASSERT(contentToMove); AutoNodeAPIWrapper nodeWrapper(aHTMLEditor, // MOZ_KnownLive because it's guaranteed by // both notifier and aClonedSiblingsToMove. MOZ_KnownLive(*contentToMove)); if (NS_FAILED(nodeWrapper.Remove())) { NS_WARNING("AutoNodeAPIWrapper::Remove() failed, but ignored"); } else { NS_WARNING_ASSERTION( nodeWrapper.IsExpectedResult(), "Temporarily removing node caused other mutations, but ignored"); } } } nsresult MoveSiblingsTransaction::InsertAllSiblingsToMove( HTMLEditor& aHTMLEditor, const nsTArray>& aClonedSiblingsToMove, nsINode& aParentNode, nsIContent* aReferenceNode, AutoMoveNodeSelNotify& aNotifier) const { MOZ_ASSERT(mHTMLEditor); nsresult rv = NS_SUCCESS_DOM_NO_OPERATION; for (const size_t i : IntegerRange(aNotifier.MovingContentCount())) { nsIContent* const contentToMove = aNotifier.GetContentAt(i); MOZ_ASSERT(contentToMove); if (Element* const elementToMove = Element::FromNodeOrNull(contentToMove)) { if (!elementToMove->HasAttr(nsGkAtoms::mozdirty)) { nsresult rvMarkElementDirty = aHTMLEditor.MarkElementDirty( MOZ_KnownLive(*contentToMove->AsElement())); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvMarkElementDirty), "EditorBase::MarkElementDirty() failed, but ignored"); (void)rvMarkElementDirty; } } AutoNodeAPIWrapper nodeWrapper(aHTMLEditor, aParentNode); // MOZ_KnownLive because of guaranteed by both aNotifier and // aClonedSiblingsToMove. nsresult rvInner = nodeWrapper.InsertBefore(MOZ_KnownLive(*contentToMove), aReferenceNode); if (NS_FAILED(rvInner)) { NS_WARNING("AutoNodeAPIWrapper::InsertBefore() failed"); rv = rvInner; } else { NS_WARNING_ASSERTION(nodeWrapper.IsExpectedResult(), "Moving a node caused other mutations, but ignored"); } } Document* const document = aHTMLEditor.GetDocument(); for (const size_t i : IntegerRange(aNotifier.MovingContentCount())) { nsIContent* const content = aNotifier.GetContentAt(i); MOZ_ASSERT(content); if (MOZ_LIKELY(content->GetParentNode() && content->OwnerDoc() == document)) { aNotifier.DidMoveContent(*content); } } return NS_WARN_IF(aHTMLEditor.Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : rv; } } // namespace mozilla