replaced by .editorconfig # ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D287844
400 lines
16 KiB
C++
400 lines
16 KiB
C++
/* 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>
|
|
MoveSiblingsTransaction::MaybeCreate(HTMLEditor& aHTMLEditor,
|
|
nsIContent& aFirstContentToMove,
|
|
nsIContent& aLastContentToMove,
|
|
const EditorDOMPoint& aPointToInsert);
|
|
template already_AddRefed<MoveSiblingsTransaction>
|
|
MoveSiblingsTransaction::MaybeCreate(HTMLEditor& aHTMLEditor,
|
|
nsIContent& aFirstContentToMove,
|
|
nsIContent& aLastContentToMove,
|
|
const EditorRawDOMPoint& aPointToInsert);
|
|
|
|
// static
|
|
template <typename PT, typename CT>
|
|
already_AddRefed<MoveSiblingsTransaction> MoveSiblingsTransaction::MaybeCreate(
|
|
HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove,
|
|
nsIContent& aLastContentToMove,
|
|
const EditorDOMPointBase<PT, CT>& 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<MoveSiblingsTransaction> transaction = new MoveSiblingsTransaction(
|
|
aHTMLEditor, aFirstContentToMove, aLastContentToMove, numberOfSiblings,
|
|
aPointToInsert);
|
|
return transaction.forget();
|
|
}
|
|
|
|
template <typename PT, typename CT>
|
|
MoveSiblingsTransaction::MoveSiblingsTransaction(
|
|
HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove,
|
|
nsIContent& aLastContentToMove, uint32_t aNumberOfSiblings,
|
|
const EditorDOMPointBase<PT, CT>& aPointToInsert)
|
|
: MoveNodeTransactionBase(aHTMLEditor, aLastContentToMove,
|
|
aPointToInsert.template To<EditorRawDOMPoint>()) {
|
|
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> htmlEditor = *mHTMLEditor;
|
|
const OwningNonNull<nsINode> newContainer = *mContainer;
|
|
const nsCOMPtr<nsIContent> newNextSibling = mReference;
|
|
const CopyableAutoTArray<OwningNonNull<nsIContent>, 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> htmlEditor = *mHTMLEditor;
|
|
const OwningNonNull<nsINode> oldContainer = *mOldContainer;
|
|
const nsCOMPtr<nsIContent> oldNextSibling = mOldNextSibling;
|
|
const CopyableAutoTArray<OwningNonNull<nsIContent>, 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<nsIContent>& 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<nsIContent>& content : Reversed(mSiblingsToMove)) {
|
|
if (MOZ_LIKELY(content->GetParentNode() == expectedContainer)) {
|
|
return content;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void MoveSiblingsTransaction::RemoveAllSiblingsToMove(
|
|
HTMLEditor& aHTMLEditor,
|
|
const nsTArray<OwningNonNull<nsIContent>>& 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<nsIContent>& 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<OwningNonNull<nsIContent>>& 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
|