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/. */
6 #ifndef mozilla_EditorUtils_h
7 #define mozilla_EditorUtils_h
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/Element.h" // for dom::Element
16 #include "mozilla/dom/HTMLBRElement.h" // for dom::HTMLBRElement
17 #include "mozilla/dom/Selection.h" // for dom::Selection
18 #include "mozilla/dom/Text.h" // for dom::Text
20 #include "nsAtom.h" // for nsStaticAtom
21 #include "nsCOMPtr.h" // for nsCOMPtr
22 #include "nsContentUtils.h" // for nsContentUtils
23 #include "nsDebug.h" // for NS_WARNING, etc
24 #include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_*
25 #include "nsRange.h" // for nsRange
26 #include "nsString.h" // for nsAString, nsString, etc
28 class nsITransferable
;
32 enum class StyleWhiteSpace
: uint8_t;
34 enum class SuggestCaret
{
35 // If specified, the method returns NS_OK when there is no recommended caret
38 // If specified and if EditorBase::AllowsTransactionsToChangeSelection
39 // returns false, the method does nothing and returns NS_OK.
40 OnlyIfTransactionsAllowedToDoIt
,
41 // If specified, the method returns
42 // NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR even if
43 // EditorBase::CollapseSelectionTo returns an error except when
44 // NS_ERROR_EDITOR_DESTROYED.
45 AndIgnoreTrivialError
,
48 /******************************************************************************
49 * CaretPoint is a wrapper of EditorDOMPoint and provides a helper method to
50 * collapse Selection there, or move it to a local variable. This is typically
51 * used as the ok type of Result or a base class of DoSomethingResult classes.
52 ******************************************************************************/
53 class MOZ_STACK_CLASS CaretPoint
{
55 explicit CaretPoint(const EditorDOMPoint
& aPointToPutCaret
)
56 : mCaretPoint(aPointToPutCaret
) {}
57 explicit CaretPoint(EditorDOMPoint
&& aPointToPutCaret
)
58 : mCaretPoint(std::move(aPointToPutCaret
)) {}
60 CaretPoint(const CaretPoint
&) = delete;
61 CaretPoint
& operator=(const CaretPoint
&) = delete;
62 CaretPoint(CaretPoint
&&) = default;
63 CaretPoint
& operator=(CaretPoint
&&) = default;
66 * Suggest caret position to aEditorBase.
68 [[nodiscard
]] MOZ_CAN_RUN_SCRIPT nsresult
SuggestCaretPointTo(
69 EditorBase
& aEditorBase
, const SuggestCaretOptions
& aOptions
) const;
72 * IgnoreCaretPointSuggestion() should be called if the method does not want
73 * to use caret position recommended by this instance.
75 void IgnoreCaretPointSuggestion() const { mHandledCaretPoint
= true; }
78 * When propagating the result, it may not want to the caller modify
79 * selection. In such case, this can clear the caret point. Use
80 * IgnoreCaretPointSuggestion() in the caller side instead.
82 void ForgetCaretPointSuggestion() { mCaretPoint
.Clear(); }
84 bool HasCaretPointSuggestion() const { return mCaretPoint
.IsSet(); }
85 constexpr const EditorDOMPoint
& CaretPointRef() const { return mCaretPoint
; }
86 constexpr EditorDOMPoint
&& UnwrapCaretPoint() {
87 mHandledCaretPoint
= true;
88 return std::move(mCaretPoint
);
90 bool CopyCaretPointTo(EditorDOMPoint
& aPointToPutCaret
,
91 const SuggestCaretOptions
& aOptions
) const {
92 MOZ_ASSERT(!aOptions
.contains(SuggestCaret::AndIgnoreTrivialError
));
94 !aOptions
.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt
));
95 mHandledCaretPoint
= true;
96 if (aOptions
.contains(SuggestCaret::OnlyIfHasSuggestion
) &&
97 !mCaretPoint
.IsSet()) {
100 aPointToPutCaret
= mCaretPoint
;
103 bool MoveCaretPointTo(EditorDOMPoint
& aPointToPutCaret
,
104 const SuggestCaretOptions
& aOptions
) {
105 MOZ_ASSERT(!aOptions
.contains(SuggestCaret::AndIgnoreTrivialError
));
107 !aOptions
.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt
));
108 if (aOptions
.contains(SuggestCaret::OnlyIfHasSuggestion
) &&
109 !mCaretPoint
.IsSet()) {
112 aPointToPutCaret
= UnwrapCaretPoint();
115 bool CopyCaretPointTo(EditorDOMPoint
& aPointToPutCaret
,
116 const EditorBase
& aEditorBase
,
117 const SuggestCaretOptions
& aOptions
) const;
118 bool MoveCaretPointTo(EditorDOMPoint
& aPointToPutCaret
,
119 const EditorBase
& aEditorBase
,
120 const SuggestCaretOptions
& aOptions
);
123 constexpr bool CaretPointHandled() const { return mHandledCaretPoint
; }
125 void SetCaretPoint(const EditorDOMPoint
& aCaretPoint
) {
126 mHandledCaretPoint
= false;
127 mCaretPoint
= aCaretPoint
;
129 void SetCaretPoint(EditorDOMPoint
&& aCaretPoint
) {
130 mHandledCaretPoint
= false;
131 mCaretPoint
= std::move(aCaretPoint
);
134 void UnmarkAsHandledCaretPoint() { mHandledCaretPoint
= true; }
136 CaretPoint() = default;
139 EditorDOMPoint mCaretPoint
;
140 bool mutable mHandledCaretPoint
= false;
143 /***************************************************************************
144 * EditActionResult is useful to return the handling state of edit sub actions
145 * without out params.
147 class MOZ_STACK_CLASS EditActionResult final
{
149 bool Canceled() const { return mCanceled
; }
150 bool Handled() const { return mHandled
; }
151 bool Ignored() const { return !mCanceled
&& !mHandled
; }
153 void MarkAsCanceled() { mCanceled
= true; }
154 void MarkAsHandled() { mHandled
= true; }
156 EditActionResult
& operator|=(const EditActionResult
& aOther
) {
157 mCanceled
|= aOther
.mCanceled
;
158 mHandled
|= aOther
.mHandled
;
162 EditActionResult
& operator|=(const MoveNodeResult
& aMoveNodeResult
);
164 static EditActionResult
IgnoredResult() {
165 return EditActionResult(false, false);
167 static EditActionResult
HandledResult() {
168 return EditActionResult(false, true);
170 static EditActionResult
CanceledResult() {
171 return EditActionResult(true, true);
174 EditActionResult(const EditActionResult
&) = delete;
175 EditActionResult
& operator=(const EditActionResult
&) = delete;
176 EditActionResult(EditActionResult
&&) = default;
177 EditActionResult
& operator=(EditActionResult
&&) = default;
180 bool mCanceled
= false;
181 bool mHandled
= false;
183 EditActionResult(bool aCanceled
, bool aHandled
)
184 : mCanceled(aCanceled
), mHandled(aHandled
) {}
186 EditActionResult() : mCanceled(false), mHandled(false) {}
189 /***************************************************************************
190 * CreateNodeResultBase is a simple class for CreateSomething() methods
191 * which want to return new node.
193 template <typename NodeType
>
194 class MOZ_STACK_CLASS CreateNodeResultBase final
: public CaretPoint
{
195 using SelfType
= CreateNodeResultBase
<NodeType
>;
198 bool Handled() const { return mNode
; }
199 NodeType
* GetNewNode() const { return mNode
; }
200 RefPtr
<NodeType
> UnwrapNewNode() { return std::move(mNode
); }
202 CreateNodeResultBase() = delete;
203 explicit CreateNodeResultBase(NodeType
& aNode
) : mNode(&aNode
) {}
204 explicit CreateNodeResultBase(NodeType
& aNode
,
205 const EditorDOMPoint
& aCandidateCaretPoint
)
206 : CaretPoint(aCandidateCaretPoint
), mNode(&aNode
) {}
207 explicit CreateNodeResultBase(NodeType
& aNode
,
208 EditorDOMPoint
&& aCandidateCaretPoint
)
209 : CaretPoint(std::move(aCandidateCaretPoint
)), mNode(&aNode
) {}
211 explicit CreateNodeResultBase(RefPtr
<NodeType
>&& aNode
)
212 : mNode(std::move(aNode
)) {}
213 explicit CreateNodeResultBase(RefPtr
<NodeType
>&& aNode
,
214 const EditorDOMPoint
& aCandidateCaretPoint
)
215 : CaretPoint(aCandidateCaretPoint
), mNode(std::move(aNode
)) {
218 explicit CreateNodeResultBase(RefPtr
<NodeType
>&& aNode
,
219 EditorDOMPoint
&& aCandidateCaretPoint
)
220 : CaretPoint(std::move(aCandidateCaretPoint
)), mNode(std::move(aNode
)) {
224 [[nodiscard
]] static SelfType
NotHandled() {
225 return SelfType(EditorDOMPoint());
227 [[nodiscard
]] static SelfType
NotHandled(
228 const EditorDOMPoint
& aPointToPutCaret
) {
229 SelfType
result(aPointToPutCaret
);
232 [[nodiscard
]] static SelfType
NotHandled(EditorDOMPoint
&& aPointToPutCaret
) {
233 SelfType
result(std::move(aPointToPutCaret
));
238 ~CreateNodeResultBase() {
239 MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
243 CreateNodeResultBase(const SelfType
& aOther
) = delete;
244 SelfType
& operator=(const SelfType
& aOther
) = delete;
245 CreateNodeResultBase(SelfType
&& aOther
) = default;
246 SelfType
& operator=(SelfType
&& aOther
) = default;
249 explicit CreateNodeResultBase(const EditorDOMPoint
& aCandidateCaretPoint
)
250 : CaretPoint(aCandidateCaretPoint
) {}
251 explicit CreateNodeResultBase(EditorDOMPoint
&& aCandidateCaretPoint
)
252 : CaretPoint(std::move(aCandidateCaretPoint
)) {}
254 RefPtr
<NodeType
> mNode
;
258 * This is a result of inserting text. If the text inserted as a part of
259 * composition, this does not return CaretPoint. Otherwise, must return
260 * CaretPoint which is typically same as end of inserted text.
262 class MOZ_STACK_CLASS InsertTextResult final
: public CaretPoint
{
264 InsertTextResult() : CaretPoint(EditorDOMPoint()) {}
265 template <typename EditorDOMPointType
>
266 explicit InsertTextResult(const EditorDOMPointType
& aEndOfInsertedText
)
267 : CaretPoint(EditorDOMPoint()),
268 mEndOfInsertedText(aEndOfInsertedText
.template To
<EditorDOMPoint
>()) {}
269 explicit InsertTextResult(EditorDOMPointInText
&& aEndOfInsertedText
)
270 : CaretPoint(EditorDOMPoint()),
271 mEndOfInsertedText(std::move(aEndOfInsertedText
)) {}
272 template <typename PT
, typename CT
>
273 InsertTextResult(EditorDOMPointInText
&& aEndOfInsertedText
,
274 const EditorDOMPointBase
<PT
, CT
>& aCaretPoint
)
275 : CaretPoint(aCaretPoint
.template To
<EditorDOMPoint
>()),
276 mEndOfInsertedText(std::move(aEndOfInsertedText
)) {}
277 InsertTextResult(EditorDOMPointInText
&& aEndOfInsertedText
,
278 CaretPoint
&& aCaretPoint
)
279 : CaretPoint(std::move(aCaretPoint
)),
280 mEndOfInsertedText(std::move(aEndOfInsertedText
)) {
281 UnmarkAsHandledCaretPoint();
283 InsertTextResult(InsertTextResult
&& aOther
, EditorDOMPoint
&& aCaretPoint
)
284 : CaretPoint(std::move(aCaretPoint
)),
285 mEndOfInsertedText(std::move(aOther
.mEndOfInsertedText
)) {}
287 [[nodiscard
]] bool Handled() const { return mEndOfInsertedText
.IsSet(); }
288 const EditorDOMPointInText
& EndOfInsertedTextRef() const {
289 return mEndOfInsertedText
;
293 EditorDOMPointInText mEndOfInsertedText
;
296 /***************************************************************************
297 * stack based helper class for calling EditorBase::EndTransaction() after
298 * EditorBase::BeginTransaction(). This shouldn't be used in editor classes
299 * or helper classes while an edit action is being handled. Use
300 * AutoTransactionBatch in such cases since it uses non-virtual internal
302 ***************************************************************************/
303 class MOZ_RAII AutoTransactionBatchExternal final
{
305 MOZ_CAN_RUN_SCRIPT
explicit AutoTransactionBatchExternal(
306 EditorBase
& aEditorBase
)
307 : mEditorBase(aEditorBase
) {
308 MOZ_KnownLive(mEditorBase
).BeginTransaction();
311 MOZ_CAN_RUN_SCRIPT
~AutoTransactionBatchExternal() {
312 MOZ_KnownLive(mEditorBase
).EndTransaction();
316 EditorBase
& mEditorBase
;
319 /******************************************************************************
320 * AutoSelectionRangeArray stores all ranges in `aSelection`.
321 * Note that modifying the ranges means modifing the selection ranges.
322 *****************************************************************************/
323 class MOZ_STACK_CLASS AutoSelectionRangeArray final
{
325 explicit AutoSelectionRangeArray(dom::Selection
& aSelection
) {
326 for (const uint32_t i
: IntegerRange(aSelection
.RangeCount())) {
327 MOZ_ASSERT(aSelection
.GetRangeAt(i
));
328 mRanges
.AppendElement(*aSelection
.GetRangeAt(i
));
332 AutoTArray
<mozilla::OwningNonNull
<nsRange
>, 8> mRanges
;
335 class EditorUtils final
{
337 using EditorType
= EditorBase::EditorType
;
338 using Selection
= dom::Selection
;
341 * IsDescendantOf() checks if aNode is a child or a descendant of aParent.
342 * aOutPoint is set to the child of aParent.
344 * @return true if aNode is a child or a descendant of aParent.
346 static bool IsDescendantOf(const nsINode
& aNode
, const nsINode
& aParent
,
347 EditorRawDOMPoint
* aOutPoint
= nullptr);
348 static bool IsDescendantOf(const nsINode
& aNode
, const nsINode
& aParent
,
349 EditorDOMPoint
* aOutPoint
);
352 * Returns true if aContent is a <br> element and it's marked as padding for
355 static bool IsPaddingBRElementForEmptyEditor(const nsIContent
& aContent
) {
356 const dom::HTMLBRElement
* brElement
=
357 dom::HTMLBRElement::FromNode(&aContent
);
358 return brElement
&& brElement
->IsPaddingForEmptyEditor();
362 * Returns true if aContent is a <br> element and it's marked as padding for
365 static bool IsPaddingBRElementForEmptyLastLine(const nsIContent
& aContent
) {
366 const dom::HTMLBRElement
* brElement
=
367 dom::HTMLBRElement::FromNode(&aContent
);
368 return brElement
&& brElement
->IsPaddingForEmptyLastLine();
372 * IsEditableContent() returns true if aContent's data or children is ediable
373 * for the given editor type. Be aware, returning true does NOT mean the
374 * node can be removed from its parent node, and returning false does NOT
375 * mean the node cannot be removed from the parent node.
376 * XXX May be the anonymous nodes in TextEditor not editable? If it's not
377 * so, we can get rid of aEditorType.
379 static bool IsEditableContent(const nsIContent
& aContent
,
380 EditorType aEditorType
) {
381 if (aEditorType
== EditorType::HTML
&&
382 (!aContent
.IsEditable() || !aContent
.IsInComposedDoc())) {
383 // FIXME(emilio): Why only for HTML editors? All content from the root
384 // content in text editors is also editable, so afaict we can remove the
388 return IsElementOrText(aContent
);
392 * Returns true if aContent is a usual element node (not padding <br> element
393 * for empty editor) or a text node. In other words, returns true if
394 * aContent is a usual element node or visible data node.
396 static bool IsElementOrText(const nsIContent
& aContent
) {
397 if (aContent
.IsText()) {
400 return aContent
.IsElement() && !IsPaddingBRElementForEmptyEditor(aContent
);
404 * Get computed white-space style of aContent.
406 static Maybe
<StyleWhiteSpace
> GetComputedWhiteSpaceStyle(
407 const nsIContent
& aContent
);
410 * IsWhiteSpacePreformatted() checks the style info for the node for the
411 * preformatted text style. This does NOT flush layout.
413 static bool IsWhiteSpacePreformatted(const nsIContent
& aContent
);
416 * IsNewLinePreformatted() checks whether the linefeed characters are
417 * preformatted or collapsible white-spaces. This does NOT flush layout.
419 static bool IsNewLinePreformatted(const nsIContent
& aContent
);
422 * IsOnlyNewLinePreformatted() checks whether the linefeed characters are
423 * preformated but white-spaces are collapsed, or otherwise. I.e., this
424 * returns true only when `white-space:pre-line`.
426 static bool IsOnlyNewLinePreformatted(const nsIContent
& aContent
);
428 static nsStaticAtom
* GetTagNameAtom(const nsAString
& aTagName
) {
429 if (aTagName
.IsEmpty()) {
432 nsAutoString lowerTagName
;
433 nsContentUtils::ASCIIToLower(aTagName
, lowerTagName
);
434 return NS_GetStaticAtom(lowerTagName
);
437 static nsStaticAtom
* GetAttributeAtom(const nsAString
& aAttribute
) {
438 if (aAttribute
.IsEmpty()) {
439 return nullptr; // Don't use nsGkAtoms::_empty for attribute.
441 return NS_GetStaticAtom(aAttribute
);
445 * Helper method for deletion. When this returns true, Selection will be
446 * computed with nsFrameSelection that also requires flushed layout
449 template <typename SelectionOrAutoRangeArray
>
450 static bool IsFrameSelectionRequiredToExtendSelection(
451 nsIEditor::EDirection aDirectionAndAmount
,
452 SelectionOrAutoRangeArray
& aSelectionOrAutoRangeArray
) {
453 switch (aDirectionAndAmount
) {
454 case nsIEditor::eNextWord
:
455 case nsIEditor::ePreviousWord
:
456 case nsIEditor::eToBeginningOfLine
:
457 case nsIEditor::eToEndOfLine
:
459 case nsIEditor::ePrevious
:
460 case nsIEditor::eNext
:
461 return aSelectionOrAutoRangeArray
.IsCollapsed();
468 * Returns true if aSelection includes the point in aParentContent.
470 static bool IsPointInSelection(const Selection
& aSelection
,
471 const nsINode
& aParentNode
, uint32_t aOffset
);
474 * Create an nsITransferable instance which has kTextMime and
475 * kMozTextInternal flavors.
477 static Result
<nsCOMPtr
<nsITransferable
>, nsresult
>
478 CreateTransferableForPlainText(const dom::Document
& aDocument
);
481 } // namespace mozilla
483 #endif // #ifndef mozilla_EditorUtils_h