MoveSiblingsTransaction.cpp (16672B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "MoveNodeTransaction.h" 7 8 #include "EditorBase.h" // for EditorBase 9 #include "EditorDOMAPIWrapper.h" // for AutoNodeAPIWrapper 10 #include "EditorDOMPoint.h" // for EditorDOMPoint 11 #include "HTMLEditor.h" // for HTMLEditor 12 #include "HTMLEditUtils.h" // for HTMLEditUtils 13 14 #include "mozilla/Likely.h" 15 #include "mozilla/Logging.h" 16 #include "mozilla/ToString.h" 17 18 #include "nsDebug.h" // for NS_WARNING, etc. 19 #include "nsError.h" // for NS_ERROR_NULL_POINTER, etc. 20 #include "nsIContent.h" // for nsIContent 21 #include "nsString.h" // for nsString 22 23 namespace mozilla { 24 25 using namespace dom; 26 27 template already_AddRefed<MoveSiblingsTransaction> 28 MoveSiblingsTransaction::MaybeCreate(HTMLEditor& aHTMLEditor, 29 nsIContent& aFirstContentToMove, 30 nsIContent& aLastContentToMove, 31 const EditorDOMPoint& aPointToInsert); 32 template already_AddRefed<MoveSiblingsTransaction> 33 MoveSiblingsTransaction::MaybeCreate(HTMLEditor& aHTMLEditor, 34 nsIContent& aFirstContentToMove, 35 nsIContent& aLastContentToMove, 36 const EditorRawDOMPoint& aPointToInsert); 37 38 // static 39 template <typename PT, typename CT> 40 already_AddRefed<MoveSiblingsTransaction> MoveSiblingsTransaction::MaybeCreate( 41 HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove, 42 nsIContent& aLastContentToMove, 43 const EditorDOMPointBase<PT, CT>& aPointToInsert) { 44 if (NS_WARN_IF(!aFirstContentToMove.GetParentNode()) || 45 NS_WARN_IF(&aFirstContentToMove == &aLastContentToMove) || 46 NS_WARN_IF(aFirstContentToMove.GetParentNode() != 47 aLastContentToMove.GetParentNode()) || 48 NS_WARN_IF(!aPointToInsert.IsSet())) { 49 return nullptr; 50 } 51 52 // The destination should be editable, but it may be in an orphan node 53 // or sub-tree to reduce number of DOM mutation events. In such case, 54 // we're okay to move a node into the non-editable content because we 55 // can assume that the caller will insert it into an editable element. 56 if (NS_WARN_IF(aPointToInsert.IsInComposedDoc() && 57 !HTMLEditUtils::IsSimplyEditableNode( 58 *aPointToInsert.GetContainer()))) { 59 return nullptr; 60 } 61 const uint32_t numberOfSiblings = [&]() -> uint32_t { 62 uint32_t num = 1; 63 for (nsIContent* content = aFirstContentToMove.GetNextSibling(); content; 64 content = content->GetNextSibling()) { 65 // TODO: We should not allow to move a node to improper container element. 66 // However, this is currently used to move invalid parent while 67 // processing the nodes. Therefore, treating the case as error 68 // breaks a lot. 69 if (NS_WARN_IF(content->IsInComposedDoc() && 70 !HTMLEditUtils::IsRemovableNode(*content))) { 71 return 0; 72 } 73 num++; 74 if (content == &aLastContentToMove) { 75 return num; 76 } 77 } 78 return 0; 79 }(); 80 if (NS_WARN_IF(!numberOfSiblings)) { 81 return nullptr; 82 } 83 RefPtr<MoveSiblingsTransaction> transaction = new MoveSiblingsTransaction( 84 aHTMLEditor, aFirstContentToMove, aLastContentToMove, numberOfSiblings, 85 aPointToInsert); 86 return transaction.forget(); 87 } 88 89 template <typename PT, typename CT> 90 MoveSiblingsTransaction::MoveSiblingsTransaction( 91 HTMLEditor& aHTMLEditor, nsIContent& aFirstContentToMove, 92 nsIContent& aLastContentToMove, uint32_t aNumberOfSiblings, 93 const EditorDOMPointBase<PT, CT>& aPointToInsert) 94 : MoveNodeTransactionBase(aHTMLEditor, aLastContentToMove, 95 aPointToInsert.template To<EditorRawDOMPoint>()) { 96 mSiblingsToMove.SetCapacity(aNumberOfSiblings); 97 for (nsIContent* content = &aFirstContentToMove; content; 98 content = content->GetNextSibling()) { 99 mSiblingsToMove.AppendElement(*content); 100 if (content == &aLastContentToMove) { 101 break; 102 } 103 } 104 MOZ_ASSERT(mSiblingsToMove.Length() == aNumberOfSiblings); 105 } 106 107 std::ostream& operator<<(std::ostream& aStream, 108 const MoveSiblingsTransaction& aTransaction) { 109 auto DumpNodeDetails = [&](const nsINode* aNode) { 110 if (aNode) { 111 if (aNode->IsText()) { 112 nsAutoString data; 113 aNode->AsText()->GetData(data); 114 aStream << " (#text \"" << NS_ConvertUTF16toUTF8(data).get() << "\")"; 115 } else { 116 aStream << " (" << *aNode << ")"; 117 } 118 } 119 }; 120 aStream << "{ mSiblingsToMove[0]=" << aTransaction.mSiblingsToMove[0].get(); 121 DumpNodeDetails(aTransaction.mSiblingsToMove[0]); 122 aStream << ", mSiblingsToMove[" << aTransaction.mSiblingsToMove.Length() - 1 123 << aTransaction.mSiblingsToMove.LastElement().get(); 124 DumpNodeDetails(aTransaction.mSiblingsToMove.LastElement()); 125 aStream << ", mContainer=" << aTransaction.mContainer.get(); 126 DumpNodeDetails(aTransaction.mContainer); 127 aStream << ", mReference=" << aTransaction.mReference.get(); 128 DumpNodeDetails(aTransaction.mReference); 129 aStream << ", mOldContainer=" << aTransaction.mOldContainer.get(); 130 DumpNodeDetails(aTransaction.mOldContainer); 131 aStream << ", mOldNextSibling=" << aTransaction.mOldNextSibling.get(); 132 DumpNodeDetails(aTransaction.mOldNextSibling); 133 aStream << ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }"; 134 return aStream; 135 } 136 137 NS_IMPL_CYCLE_COLLECTION_INHERITED(MoveSiblingsTransaction, EditTransactionBase, 138 mSiblingsToMove) 139 140 NS_IMPL_ADDREF_INHERITED(MoveSiblingsTransaction, EditTransactionBase) 141 NS_IMPL_RELEASE_INHERITED(MoveSiblingsTransaction, EditTransactionBase) 142 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MoveSiblingsTransaction) 143 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) 144 145 NS_IMETHODIMP MoveSiblingsTransaction::DoTransaction() { 146 MOZ_LOG(GetLogModule(), LogLevel::Info, 147 ("%p MoveSiblingsTransaction::%s this=%s", this, __FUNCTION__, 148 ToString(*this).c_str())); 149 mDone = true; 150 return DoTransactionInternal(); 151 } 152 153 nsresult MoveSiblingsTransaction::DoTransactionInternal() { 154 MOZ_DIAGNOSTIC_ASSERT(mHTMLEditor); 155 MOZ_DIAGNOSTIC_ASSERT(!mSiblingsToMove.IsEmpty()); 156 MOZ_DIAGNOSTIC_ASSERT(mContainer); 157 MOZ_DIAGNOSTIC_ASSERT(mOldContainer); 158 159 { 160 const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor; 161 const OwningNonNull<nsINode> newContainer = *mContainer; 162 const nsCOMPtr<nsIContent> newNextSibling = mReference; 163 const CopyableAutoTArray<OwningNonNull<nsIContent>, 64> siblingsToMove( 164 mSiblingsToMove); 165 AutoMoveNodeSelNotify notifier( 166 htmlEditor->RangeUpdaterRef(), 167 mReference ? EditorRawDOMPoint(mReference) 168 : EditorRawDOMPoint::AtEndOf(*newContainer)); 169 // First, remove all nodes from the DOM if they are removable. Then, 170 // IMEContentObserver can use cache to avoid to compute the start offset of 171 // each deleting text. 172 RemoveAllSiblingsToMove(htmlEditor, siblingsToMove, notifier); 173 // Next, insert all removed nodes into the DOM. Then, IMEContentObserver 174 // can use cache to avoid to compute the start offset of each inserting 175 // text. 176 InsertAllSiblingsToMove(htmlEditor, siblingsToMove, newContainer, 177 newNextSibling, notifier); 178 } 179 return NS_WARN_IF(mHTMLEditor->Destroyed()) ? NS_ERROR_EDITOR_DESTROYED 180 : NS_OK; 181 } 182 183 NS_IMETHODIMP MoveSiblingsTransaction::UndoTransaction() { 184 MOZ_LOG(GetLogModule(), LogLevel::Info, 185 ("%p MoveSiblingsTransaction::%s this=%s", this, __FUNCTION__, 186 ToString(*this).c_str())); 187 188 mDone = false; 189 190 if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(mSiblingsToMove.IsEmpty()) || 191 NS_WARN_IF(!IsSiblingsToMoveValid()) || NS_WARN_IF(!mOldContainer)) { 192 // Perhaps, nulled-out by the cycle collector. 193 return NS_ERROR_FAILURE; 194 } 195 196 // If the original point has been changed, refer mOldNextSibling if it's 197 // reasonable. Otherwise, use end of the old container. 198 if (mOldNextSibling && mOldContainer != mOldNextSibling->GetParentNode()) { 199 // TODO: Check whether the new container is proper one for containing 200 // content in mSiblingsToMove. However, there are few testcases so 201 // that we shouldn't change here without creating a lot of undo tests. 202 if (mOldNextSibling->GetParentNode() && 203 (mOldNextSibling->IsInComposedDoc() || 204 !mOldContainer->IsInComposedDoc())) { 205 mOldContainer = mOldNextSibling->GetParentNode(); 206 } else { 207 mOldNextSibling = nullptr; // end of mOldContainer 208 } 209 } 210 211 if (MOZ_UNLIKELY(mOldContainer->IsInComposedDoc() && 212 !HTMLEditUtils::IsSimplyEditableNode(*mOldContainer))) { 213 NS_WARNING( 214 "MoveSiblingsTransaction::UndoTransaction() couldn't move the " 215 "content into the old container due to non-editable one"); 216 return NS_ERROR_FAILURE; 217 } 218 219 // And store the latest node which should be referred at redoing. 220 mContainer = mSiblingsToMove.LastElement()->GetParentNode(); 221 mReference = mSiblingsToMove.LastElement()->GetNextSibling(); 222 223 { 224 const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor; 225 const OwningNonNull<nsINode> oldContainer = *mOldContainer; 226 const nsCOMPtr<nsIContent> oldNextSibling = mOldNextSibling; 227 const CopyableAutoTArray<OwningNonNull<nsIContent>, 64> siblingsToMove( 228 mSiblingsToMove); 229 AutoMoveNodeSelNotify notifier( 230 htmlEditor->RangeUpdaterRef(), 231 oldNextSibling ? EditorRawDOMPoint(oldNextSibling) 232 : EditorRawDOMPoint::AtEndOf(*oldContainer)); 233 // First, remove all nodes from the DOM if they are removable. Then, 234 // IMEContentObserver can use cache to avoid to compute the start offset of 235 // each deleting text. 236 RemoveAllSiblingsToMove(htmlEditor, siblingsToMove, notifier); 237 // Next, insert all removed nodes into the DOM. Then, IMEContentObserver 238 // can use cache to avoid to compute the start offset of each inserting 239 // text. 240 InsertAllSiblingsToMove(htmlEditor, siblingsToMove, oldContainer, 241 oldNextSibling, notifier); 242 } 243 244 return NS_WARN_IF(mHTMLEditor->Destroyed()) ? NS_ERROR_EDITOR_DESTROYED 245 : NS_OK; 246 } 247 248 NS_IMETHODIMP MoveSiblingsTransaction::RedoTransaction() { 249 MOZ_LOG(GetLogModule(), LogLevel::Info, 250 ("%p MoveSiblingsTransaction::%s this=%s", this, __FUNCTION__, 251 ToString(*this).c_str())); 252 253 mDone = true; 254 255 if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(mSiblingsToMove.IsEmpty()) || 256 NS_WARN_IF(!IsSiblingsToMoveValid()) || NS_WARN_IF(!mContainer)) { 257 // Perhaps, nulled-out by the cycle collector. 258 return NS_ERROR_FAILURE; 259 } 260 261 // If the inserting point has been changed, refer mReference if it's 262 // reasonable. Otherwise, use end of the container. 263 if (mReference && mContainer != mReference->GetParentNode()) { 264 // TODO: Check whether the new container is proper one for containing 265 // mContentToMove. However, there are few testcases so that we 266 // shouldn't change here without creating a lot of redo tests. 267 if (mReference->GetParentNode() && 268 (mReference->IsInComposedDoc() || !mContainer->IsInComposedDoc())) { 269 mContainer = mReference->GetParentNode(); 270 } else { 271 mReference = nullptr; // end of mContainer 272 } 273 } 274 275 if (MOZ_UNLIKELY(mContainer->IsInComposedDoc() && 276 !HTMLEditUtils::IsSimplyEditableNode(*mContainer))) { 277 NS_WARNING( 278 "MoveSiblingsTransaction::RedoTransaction() couldn't move the " 279 "content into the new container due to non-editable one"); 280 return NS_ERROR_FAILURE; 281 } 282 283 // And store the latest node which should be back. 284 mOldContainer = mSiblingsToMove.LastElement()->GetParentNode(); 285 mOldNextSibling = mSiblingsToMove.LastElement()->GetNextSibling(); 286 287 nsresult rv = DoTransactionInternal(); 288 if (NS_FAILED(rv)) { 289 NS_WARNING("MoveSiblingsTransaction::DoTransactionInternal() failed"); 290 return rv; 291 } 292 return NS_OK; 293 } 294 295 nsIContent* MoveSiblingsTransaction::GetFirstMovedContent() const { 296 nsINode* const expectedContainer = mDone ? mContainer : mOldContainer; 297 for (const OwningNonNull<nsIContent>& content : mSiblingsToMove) { 298 if (MOZ_LIKELY(content->GetParentNode() == expectedContainer)) { 299 return content; 300 } 301 } 302 return nullptr; 303 } 304 305 nsIContent* MoveSiblingsTransaction::GetLastMovedContent() const { 306 nsINode* const expectedContainer = mDone ? mContainer : mOldContainer; 307 for (const OwningNonNull<nsIContent>& content : Reversed(mSiblingsToMove)) { 308 if (MOZ_LIKELY(content->GetParentNode() == expectedContainer)) { 309 return content; 310 } 311 } 312 return nullptr; 313 } 314 315 void MoveSiblingsTransaction::RemoveAllSiblingsToMove( 316 HTMLEditor& aHTMLEditor, 317 const nsTArray<OwningNonNull<nsIContent>>& aClonedSiblingsToMove, 318 AutoMoveNodeSelNotify& aNotifier) const { 319 // Be aware, if we're undoing or redoing, some aClonedSiblingsToMove may not 320 // be the adjacent sibling of prev/next element in the array. Therefore, we 321 // may need to compute the index within the expensive path. 322 323 // First, we need to make AutoMoveNodeSelNotify instances store all indices of 324 // the moving content nodes. 325 { 326 for (const OwningNonNull<nsIContent>& contentToMove : 327 aClonedSiblingsToMove) { 328 if (contentToMove->IsInComposedDoc() && 329 !HTMLEditUtils::IsRemovableNode(contentToMove)) { 330 continue; 331 } 332 aNotifier.AppendContentWhichWillBeMoved(contentToMove); 333 } 334 } 335 // Then, remove all nodes unless not removable. 336 for (const size_t i : IntegerRange(aNotifier.MovingContentCount())) { 337 nsIContent* const contentToMove = aNotifier.GetContentAt(i); 338 MOZ_ASSERT(contentToMove); 339 AutoNodeAPIWrapper nodeWrapper(aHTMLEditor, 340 // MOZ_KnownLive because it's guaranteed by 341 // both notifier and aClonedSiblingsToMove. 342 MOZ_KnownLive(*contentToMove)); 343 if (NS_FAILED(nodeWrapper.Remove())) { 344 NS_WARNING("AutoNodeAPIWrapper::Remove() failed, but ignored"); 345 } else { 346 NS_WARNING_ASSERTION( 347 nodeWrapper.IsExpectedResult(), 348 "Temporarily removing node caused other mutations, but ignored"); 349 } 350 } 351 } 352 353 nsresult MoveSiblingsTransaction::InsertAllSiblingsToMove( 354 HTMLEditor& aHTMLEditor, 355 const nsTArray<OwningNonNull<nsIContent>>& aClonedSiblingsToMove, 356 nsINode& aParentNode, nsIContent* aReferenceNode, 357 AutoMoveNodeSelNotify& aNotifier) const { 358 MOZ_ASSERT(mHTMLEditor); 359 nsresult rv = NS_SUCCESS_DOM_NO_OPERATION; 360 for (const size_t i : IntegerRange(aNotifier.MovingContentCount())) { 361 nsIContent* const contentToMove = aNotifier.GetContentAt(i); 362 MOZ_ASSERT(contentToMove); 363 if (Element* const elementToMove = Element::FromNodeOrNull(contentToMove)) { 364 if (!elementToMove->HasAttr(nsGkAtoms::mozdirty)) { 365 nsresult rvMarkElementDirty = aHTMLEditor.MarkElementDirty( 366 MOZ_KnownLive(*contentToMove->AsElement())); 367 NS_WARNING_ASSERTION( 368 NS_SUCCEEDED(rvMarkElementDirty), 369 "EditorBase::MarkElementDirty() failed, but ignored"); 370 (void)rvMarkElementDirty; 371 } 372 } 373 374 AutoNodeAPIWrapper nodeWrapper(aHTMLEditor, aParentNode); 375 // MOZ_KnownLive because of guaranteed by both aNotifier and 376 // aClonedSiblingsToMove. 377 nsresult rvInner = 378 nodeWrapper.InsertBefore(MOZ_KnownLive(*contentToMove), aReferenceNode); 379 if (NS_FAILED(rvInner)) { 380 NS_WARNING("AutoNodeAPIWrapper::InsertBefore() failed"); 381 rv = rvInner; 382 } else { 383 NS_WARNING_ASSERTION(nodeWrapper.IsExpectedResult(), 384 "Moving a node caused other mutations, but ignored"); 385 } 386 } 387 388 Document* const document = aHTMLEditor.GetDocument(); 389 for (const size_t i : IntegerRange(aNotifier.MovingContentCount())) { 390 nsIContent* const content = aNotifier.GetContentAt(i); 391 MOZ_ASSERT(content); 392 if (MOZ_LIKELY(content->GetParentNode() && 393 content->OwnerDoc() == document)) { 394 aNotifier.DidMoveContent(*content); 395 } 396 } 397 return NS_WARN_IF(aHTMLEditor.Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : rv; 398 } 399 400 } // namespace mozilla