EditorUtils.h (20163B)
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 #ifndef mozilla_EditorUtils_h 7 #define mozilla_EditorUtils_h 8 9 #include "mozilla/EditorBase.h" // for EditorBase 10 #include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc 11 #include "mozilla/EditorForwards.h" 12 #include "mozilla/IntegerRange.h" // for IntegerRange 13 #include "mozilla/Maybe.h" // for Maybe 14 #include "mozilla/Result.h" // for Result<> 15 #include "mozilla/dom/DataTransfer.h" // for dom::DataTransfer 16 #include "mozilla/dom/Element.h" // for dom::Element 17 #include "mozilla/dom/HTMLBRElement.h" // for dom::HTMLBRElement 18 #include "mozilla/dom/Selection.h" // for dom::Selection 19 #include "mozilla/dom/Text.h" // for dom::Text 20 21 #include "nsAtom.h" // for nsStaticAtom 22 #include "nsCOMPtr.h" // for nsCOMPtr 23 #include "nsContentUtils.h" // for nsContentUtils 24 #include "nsDebug.h" // for NS_WARNING, etc 25 #include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_* 26 #include "nsRange.h" // for nsRange 27 #include "nsString.h" // for nsAString, nsString, etc 28 29 class nsITransferable; 30 31 namespace mozilla { 32 33 enum class StyleWhiteSpace : uint8_t; 34 35 enum class SuggestCaret { 36 // If specified, the method returns NS_OK when there is no recommended caret 37 // position. 38 OnlyIfHasSuggestion, 39 // If specified and if EditorBase::AllowsTransactionsToChangeSelection 40 // returns false, the method does nothing and returns NS_OK. 41 OnlyIfTransactionsAllowedToDoIt, 42 // If specified, the method returns 43 // NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR even if 44 // EditorBase::CollapseSelectionTo returns an error except when 45 // NS_ERROR_EDITOR_DESTROYED. 46 AndIgnoreTrivialError, 47 }; 48 49 /****************************************************************************** 50 * CaretPoint is a wrapper of EditorDOMPoint and provides a helper method to 51 * collapse Selection there, or move it to a local variable. This is typically 52 * used as the ok type of Result or a base class of DoSomethingResult classes. 53 ******************************************************************************/ 54 class MOZ_STACK_CLASS CaretPoint { 55 public: 56 explicit CaretPoint(const EditorDOMPoint& aPointToPutCaret) 57 : mCaretPoint(aPointToPutCaret) {} 58 explicit CaretPoint(EditorDOMPoint&& aPointToPutCaret) 59 : mCaretPoint(std::move(aPointToPutCaret)) {} 60 61 CaretPoint(const CaretPoint&) = delete; 62 CaretPoint& operator=(const CaretPoint&) = delete; 63 CaretPoint(CaretPoint&&) = default; 64 CaretPoint& operator=(CaretPoint&&) = default; 65 66 /** 67 * Suggest caret position to aEditorBase. 68 */ 69 [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SuggestCaretPointTo( 70 EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) const; 71 72 /** 73 * IgnoreCaretPointSuggestion() should be called if the method does not want 74 * to use caret position recommended by this instance. 75 */ 76 void IgnoreCaretPointSuggestion() const { mHandledCaretPoint = true; } 77 78 /** 79 * When propagating the result, it may not want to the caller modify 80 * selection. In such case, this can clear the caret point. Use 81 * IgnoreCaretPointSuggestion() in the caller side instead. 82 */ 83 void ForgetCaretPointSuggestion() { mCaretPoint.Clear(); } 84 85 bool HasCaretPointSuggestion() const { return mCaretPoint.IsSet(); } 86 constexpr const EditorDOMPoint& CaretPointRef() const { return mCaretPoint; } 87 constexpr EditorDOMPoint&& UnwrapCaretPoint() { 88 mHandledCaretPoint = true; 89 return std::move(mCaretPoint); 90 } 91 bool CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret, 92 const SuggestCaretOptions& aOptions) const { 93 MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError)); 94 MOZ_ASSERT( 95 !aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt)); 96 mHandledCaretPoint = true; 97 if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) && 98 !mCaretPoint.IsSet()) { 99 return false; 100 } 101 aPointToPutCaret = mCaretPoint; 102 return true; 103 } 104 bool CopyCaretPointTo(CaretPoint& aCaretPoint, 105 const SuggestCaretOptions& aOptions) const { 106 return CopyCaretPointTo(aCaretPoint.mCaretPoint, aOptions); 107 } 108 bool MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret, 109 const SuggestCaretOptions& aOptions) { 110 MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError)); 111 MOZ_ASSERT( 112 !aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt)); 113 if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) && 114 !mCaretPoint.IsSet()) { 115 return false; 116 } 117 aPointToPutCaret = UnwrapCaretPoint(); 118 return true; 119 } 120 bool MoveCaretPointTo(CaretPoint& aCaretPoint, 121 const SuggestCaretOptions& aOptions) { 122 return MoveCaretPointTo(aCaretPoint.mCaretPoint, aOptions); 123 } 124 bool CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret, 125 const EditorBase& aEditorBase, 126 const SuggestCaretOptions& aOptions) const; 127 bool MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret, 128 const EditorBase& aEditorBase, 129 const SuggestCaretOptions& aOptions); 130 131 protected: 132 constexpr bool CaretPointHandled() const { return mHandledCaretPoint; } 133 134 void SetCaretPoint(const EditorDOMPoint& aCaretPoint) { 135 mHandledCaretPoint = false; 136 mCaretPoint = aCaretPoint; 137 } 138 void SetCaretPoint(EditorDOMPoint&& aCaretPoint) { 139 mHandledCaretPoint = false; 140 mCaretPoint = std::move(aCaretPoint); 141 } 142 143 void UnmarkAsHandledCaretPoint() { mHandledCaretPoint = true; } 144 145 CaretPoint() = default; 146 147 private: 148 EditorDOMPoint mCaretPoint; 149 bool mutable mHandledCaretPoint = false; 150 151 friend class AutoTrackDOMPoint; 152 }; 153 154 /*************************************************************************** 155 * EditActionResult is useful to return the handling state of edit sub actions 156 * without out params. 157 */ 158 class MOZ_STACK_CLASS EditActionResult { 159 public: 160 bool Canceled() const { return mCanceled; } 161 bool Handled() const { return mHandled; } 162 bool Ignored() const { return !mCanceled && !mHandled; } 163 164 void MarkAsCanceled() { mCanceled = true; } 165 void MarkAsHandled() { mHandled = true; } 166 167 EditActionResult& operator|=(const EditActionResult& aOther) { 168 mCanceled |= aOther.mCanceled; 169 mHandled |= aOther.mHandled; 170 return *this; 171 } 172 173 static EditActionResult IgnoredResult() { 174 return EditActionResult(false, false); 175 } 176 static EditActionResult HandledResult() { 177 return EditActionResult(false, true); 178 } 179 static EditActionResult CanceledResult() { 180 return EditActionResult(true, true); 181 } 182 183 EditActionResult(const EditActionResult&) = delete; 184 EditActionResult& operator=(const EditActionResult&) = delete; 185 EditActionResult(EditActionResult&&) = default; 186 EditActionResult& operator=(EditActionResult&&) = default; 187 188 protected: 189 EditActionResult(bool aCanceled, bool aHandled) 190 : mCanceled(aCanceled), mHandled(aHandled) {} 191 192 EditActionResult() : mCanceled(false), mHandled(false) {} 193 194 void UnmarkAsCanceled() { mCanceled = false; } 195 196 private: 197 bool mCanceled = false; 198 bool mHandled = false; 199 }; 200 201 /*************************************************************************** 202 * CreateNodeResultBase is a simple class for CreateSomething() methods 203 * which want to return new node. 204 */ 205 template <typename NodeType> 206 class MOZ_STACK_CLASS CreateNodeResultBase final : public CaretPoint { 207 using SelfType = CreateNodeResultBase<NodeType>; 208 209 public: 210 bool Handled() const { return mNode; } 211 NodeType* GetNewNode() const { return mNode; } 212 RefPtr<NodeType> UnwrapNewNode() { return std::move(mNode); } 213 214 CreateNodeResultBase() = delete; 215 explicit CreateNodeResultBase(NodeType& aNode) : mNode(&aNode) {} 216 explicit CreateNodeResultBase(NodeType& aNode, 217 const EditorDOMPoint& aCandidateCaretPoint) 218 : CaretPoint(aCandidateCaretPoint), mNode(&aNode) {} 219 explicit CreateNodeResultBase(NodeType& aNode, 220 EditorDOMPoint&& aCandidateCaretPoint) 221 : CaretPoint(std::move(aCandidateCaretPoint)), mNode(&aNode) {} 222 223 template <typename NT> 224 explicit CreateNodeResultBase(RefPtr<NT>&& aNode) 225 : mNode(std::forward<RefPtr<NT>>(aNode)) {} 226 template <typename NT> 227 explicit CreateNodeResultBase(RefPtr<NT>&& aNode, 228 const EditorDOMPoint& aCandidateCaretPoint) 229 : CaretPoint(aCandidateCaretPoint), 230 mNode(std::forward<RefPtr<NT>>(aNode)) { 231 MOZ_ASSERT(mNode); 232 } 233 template <typename NT> 234 explicit CreateNodeResultBase(RefPtr<NT>&& aNode, 235 EditorDOMPoint&& aCandidateCaretPoint) 236 : CaretPoint(std::move(aCandidateCaretPoint)), 237 mNode(std::forward<RefPtr<NT>>(aNode)) { 238 MOZ_ASSERT(mNode); 239 } 240 241 [[nodiscard]] static SelfType NotHandled() { 242 return SelfType(EditorDOMPoint()); 243 } 244 [[nodiscard]] static SelfType NotHandled( 245 const EditorDOMPoint& aPointToPutCaret) { 246 SelfType result(aPointToPutCaret); 247 return result; 248 } 249 [[nodiscard]] static SelfType NotHandled(EditorDOMPoint&& aPointToPutCaret) { 250 SelfType result(std::move(aPointToPutCaret)); 251 return result; 252 } 253 254 #ifdef DEBUG 255 ~CreateNodeResultBase() { 256 MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled()); 257 } 258 #endif 259 260 CreateNodeResultBase(const SelfType& aOther) = delete; 261 SelfType& operator=(const SelfType& aOther) = delete; 262 CreateNodeResultBase(SelfType&& aOther) = default; 263 SelfType& operator=(SelfType&& aOther) = default; 264 265 private: 266 explicit CreateNodeResultBase(const EditorDOMPoint& aCandidateCaretPoint) 267 : CaretPoint(aCandidateCaretPoint) {} 268 explicit CreateNodeResultBase(EditorDOMPoint&& aCandidateCaretPoint) 269 : CaretPoint(std::move(aCandidateCaretPoint)) {} 270 271 RefPtr<NodeType> mNode; 272 }; 273 274 /** 275 * This is a result of inserting text. If the text inserted as a part of 276 * composition, this does not return CaretPoint. Otherwise, must return 277 * CaretPoint which is typically same as end of inserted text. 278 */ 279 class MOZ_STACK_CLASS InsertTextResult final : public CaretPoint { 280 public: 281 InsertTextResult() : CaretPoint(EditorDOMPoint()) {} 282 template <typename EditorDOMPointType> 283 explicit InsertTextResult(const EditorDOMPointType& aEndOfInsertedText) 284 : CaretPoint(EditorDOMPoint()), 285 mEndOfInsertedText(aEndOfInsertedText.template To<EditorDOMPoint>()) {} 286 explicit InsertTextResult(EditorDOMPoint&& aEndOfInsertedText) 287 : CaretPoint(EditorDOMPoint()), 288 mEndOfInsertedText(std::move(aEndOfInsertedText)) {} 289 template <typename PT, typename CT> 290 InsertTextResult(EditorDOMPoint&& aEndOfInsertedText, 291 const EditorDOMPointBase<PT, CT>& aCaretPoint) 292 : CaretPoint(aCaretPoint.template To<EditorDOMPoint>()), 293 mEndOfInsertedText(std::move(aEndOfInsertedText)) {} 294 InsertTextResult(EditorDOMPoint&& aEndOfInsertedText, 295 CaretPoint&& aCaretPoint) 296 : CaretPoint(std::move(aCaretPoint)), 297 mEndOfInsertedText(std::move(aEndOfInsertedText)) { 298 UnmarkAsHandledCaretPoint(); 299 } 300 InsertTextResult(InsertTextResult&& aOther, EditorDOMPoint&& aCaretPoint) 301 : CaretPoint(std::move(aCaretPoint)), 302 mEndOfInsertedText(std::move(aOther.mEndOfInsertedText)) {} 303 304 [[nodiscard]] bool Handled() const { return mEndOfInsertedText.IsSet(); } 305 const EditorDOMPoint& EndOfInsertedTextRef() const { 306 return mEndOfInsertedText; 307 } 308 309 private: 310 EditorDOMPoint mEndOfInsertedText; 311 }; 312 313 /*************************************************************************** 314 * stack based helper class for calling EditorBase::EndTransaction() after 315 * EditorBase::BeginTransaction(). This shouldn't be used in editor classes 316 * or helper classes while an edit action is being handled. Use 317 * AutoTransactionBatch in such cases since it uses non-virtual internal 318 * methods. 319 ***************************************************************************/ 320 class MOZ_RAII AutoTransactionBatchExternal final { 321 public: 322 MOZ_CAN_RUN_SCRIPT explicit AutoTransactionBatchExternal( 323 EditorBase& aEditorBase) 324 : mEditorBase(aEditorBase) { 325 MOZ_KnownLive(mEditorBase).BeginTransaction(); 326 } 327 328 MOZ_CAN_RUN_SCRIPT ~AutoTransactionBatchExternal() { 329 MOZ_KnownLive(mEditorBase).EndTransaction(); 330 } 331 332 private: 333 EditorBase& mEditorBase; 334 }; 335 336 /****************************************************************************** 337 * AutoSelectionRangeArray stores all ranges in `aSelection`. 338 * Note that modifying the ranges means modifing the selection ranges. 339 *****************************************************************************/ 340 class MOZ_STACK_CLASS AutoSelectionRangeArray final { 341 public: 342 explicit AutoSelectionRangeArray(dom::Selection& aSelection) { 343 for (const uint32_t i : IntegerRange(aSelection.RangeCount())) { 344 MOZ_ASSERT(aSelection.GetRangeAt(i)); 345 mRanges.AppendElement(*aSelection.GetRangeAt(i)); 346 } 347 } 348 349 AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges; 350 }; 351 352 /****************************************************************************** 353 * AutoTrackDataTransferForPaste keeps track of whether the paste event handler 354 * in JS has modified the clipboard. 355 *****************************************************************************/ 356 class MOZ_STACK_CLASS AutoTrackDataTransferForPaste { 357 public: 358 MOZ_CAN_RUN_SCRIPT AutoTrackDataTransferForPaste( 359 const EditorBase& aEditorBase, 360 RefPtr<dom::DataTransfer>& aDataTransferForPaste) 361 : mEditorBase(aEditorBase), 362 mDataTransferForPaste(aDataTransferForPaste.get_address()) { 363 mEditorBase.GetDocument()->ClearClipboardCopyTriggered(); 364 } 365 366 ~AutoTrackDataTransferForPaste() { FlushAndStopTracking(); } 367 368 private: 369 void FlushAndStopTracking() { 370 if (!mDataTransferForPaste || 371 !mEditorBase.GetDocument()->IsClipboardCopyTriggered()) { 372 return; 373 } 374 // The paste event copied new data to the clipboard, so we need to use 375 // that data to paste into the DOM element below. 376 if (*mDataTransferForPaste) { 377 (*mDataTransferForPaste)->ClearForPaste(); 378 } 379 // Just null this out so this data won't be used and we will get it directly 380 // from the clipboard in the future. 381 *mDataTransferForPaste = nullptr; 382 mDataTransferForPaste = nullptr; 383 } 384 385 MOZ_KNOWN_LIVE const EditorBase& mEditorBase; 386 RefPtr<dom::DataTransfer>* mDataTransferForPaste; 387 }; 388 389 class EditorUtils final { 390 public: 391 using EditorType = EditorBase::EditorType; 392 using Selection = dom::Selection; 393 394 /** 395 * IsDescendantOf() checks if aNode is a child or a descendant of aParent. 396 * aOutPoint is set to the child of aParent. 397 * 398 * @return true if aNode is a child or a descendant of aParent. 399 */ 400 static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent, 401 EditorRawDOMPoint* aOutPoint = nullptr); 402 static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent, 403 EditorDOMPoint* aOutPoint); 404 405 /** 406 * Returns true if aContent is a <br> element and it's marked as padding for 407 * empty editor. 408 */ 409 static bool IsPaddingBRElementForEmptyEditor(const nsIContent& aContent) { 410 const dom::HTMLBRElement* brElement = 411 dom::HTMLBRElement::FromNode(&aContent); 412 return brElement && brElement->IsPaddingForEmptyEditor(); 413 } 414 415 /** 416 * Returns true if aContent is a <br> element and it's marked as padding for 417 * empty last line. 418 */ 419 static bool IsPaddingBRElementForEmptyLastLine(const nsIContent& aContent) { 420 const dom::HTMLBRElement* brElement = 421 dom::HTMLBRElement::FromNode(&aContent); 422 return brElement && brElement->IsPaddingForEmptyLastLine(); 423 } 424 425 /** 426 * IsEditableContent() returns true if aContent's data or children is ediable 427 * for the given editor type. Be aware, returning true does NOT mean the 428 * node can be removed from its parent node, and returning false does NOT 429 * mean the node cannot be removed from the parent node. 430 * XXX May be the anonymous nodes in TextEditor not editable? If it's not 431 * so, we can get rid of aEditorType. 432 */ 433 static bool IsEditableContent(const nsIContent& aContent, 434 EditorType aEditorType) { 435 if (aEditorType == EditorType::HTML && 436 (!aContent.IsEditable() || !aContent.IsInComposedDoc())) { 437 // FIXME(emilio): Why only for HTML editors? All content from the root 438 // content in text editors is also editable, so afaict we can remove the 439 // special-case. 440 return false; 441 } 442 return IsElementOrText(aContent); 443 } 444 445 /** 446 * Returns true if aContent is a usual element node (not padding <br> element 447 * for empty editor) or a text node. In other words, returns true if 448 * aContent is a usual element node or visible data node. 449 */ 450 static bool IsElementOrText(const nsIContent& aContent) { 451 if (aContent.IsText()) { 452 return true; 453 } 454 return aContent.IsElement() && !IsPaddingBRElementForEmptyEditor(aContent); 455 } 456 457 /** 458 * Get the two longhands that make up computed white-space style of aContent. 459 */ 460 static Maybe<std::pair<StyleWhiteSpaceCollapse, StyleTextWrapMode>> 461 GetComputedWhiteSpaceStyles(const nsIContent& aContent); 462 463 /** 464 * IsWhiteSpacePreformatted() checks the style info for the node for the 465 * preformatted text style. This does NOT flush layout. 466 */ 467 static bool IsWhiteSpacePreformatted(const nsIContent& aContent); 468 469 /** 470 * IsNewLinePreformatted() checks whether the linefeed characters are 471 * preformatted or white-spaces. This does NOT flush layout. 472 * Be aware that even if this returns false, the linefeed characters may be 473 * rendered as non-collapsible white-spaces. Therefore, if you want to check 474 * whether linefeeds are collapsible or not, you should refer the result of 475 * IsWhiteSpacePreformatted(). 476 */ 477 static bool IsNewLinePreformatted(const nsIContent& aContent); 478 479 /** 480 * IsOnlyNewLinePreformatted() checks whether the linefeed characters are 481 * preformated but white-spaces are collapsed, or otherwise. I.e., this 482 * returns true only when `white-space-collapse:pre-line`. 483 */ 484 static bool IsOnlyNewLinePreformatted(const nsIContent& aContent); 485 486 static nsStaticAtom* GetTagNameAtom(const nsAString& aTagName) { 487 if (aTagName.IsEmpty()) { 488 return nullptr; 489 } 490 nsAutoString lowerTagName; 491 nsContentUtils::ASCIIToLower(aTagName, lowerTagName); 492 return NS_GetStaticAtom(lowerTagName); 493 } 494 495 static nsStaticAtom* GetAttributeAtom(const nsAString& aAttribute) { 496 if (aAttribute.IsEmpty()) { 497 return nullptr; // Don't use nsGkAtoms::_empty for attribute. 498 } 499 return NS_GetStaticAtom(aAttribute); 500 } 501 502 /** 503 * Helper method for deletion. When this returns true, Selection will be 504 * computed with nsFrameSelection that also requires flushed layout 505 * information. 506 */ 507 template <typename SelectionOrAutoClonedRangeArray> 508 static bool IsFrameSelectionRequiredToExtendSelection( 509 nsIEditor::EDirection aDirectionAndAmount, 510 SelectionOrAutoClonedRangeArray& aSelectionOrAutoClonedRangeArray) { 511 switch (aDirectionAndAmount) { 512 case nsIEditor::eNextWord: 513 case nsIEditor::ePreviousWord: 514 case nsIEditor::eToBeginningOfLine: 515 case nsIEditor::eToEndOfLine: 516 return true; 517 case nsIEditor::ePrevious: 518 case nsIEditor::eNext: 519 return aSelectionOrAutoClonedRangeArray.IsCollapsed(); 520 default: 521 return false; 522 } 523 } 524 525 /** 526 * Create an nsITransferable instance which has kTextMime and 527 * kMozTextInternal flavors. 528 */ 529 static Result<nsCOMPtr<nsITransferable>, nsresult> 530 CreateTransferableForPlainText(const dom::Document& aDocument); 531 }; 532 533 } // namespace mozilla 534 535 #endif // #ifndef mozilla_EditorUtils_h