1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "HTMLEditor.h"
12 #include "HTMLEditUtils.h"
13 #include "WSRunObject.h"
15 #include "mozilla/Assertions.h"
16 #include "mozilla/CSSEditUtils.h"
17 #include "mozilla/EditAction.h"
18 #include "mozilla/EditorUtils.h"
19 #include "mozilla/OwningNonNull.h"
20 #include "mozilla/dom/Element.h"
21 #include "mozilla/dom/Selection.h"
23 #include "nsAString.h"
24 #include "nsAlgorithm.h"
28 #include "nsGkAtoms.h"
29 #include "nsIContent.h"
33 #include "nsStringFwd.h"
36 // NOTE: This file was split from:
37 // https://searchfox.org/mozilla-central/rev/c409dd9235c133ab41eba635f906aa16e050c197/editor/libeditor/HTMLEditSubActionHandler.cpp
41 /*****************************************************************************
42 * ListElementSelectionState
43 ****************************************************************************/
45 ListElementSelectionState::ListElementSelectionState(HTMLEditor
& aHTMLEditor
,
47 MOZ_ASSERT(!aRv
.Failed());
49 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
50 aRv
.Throw(NS_ERROR_EDITOR_DESTROYED
);
54 // XXX Should we create another constructor which won't create
55 // AutoEditActionDataSetter? Or should we create another
56 // AutoEditActionDataSetter which won't nest edit action?
57 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
58 EditAction::eNotEditing
);
59 if (NS_WARN_IF(!editActionData
.CanHandle())) {
60 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
64 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
65 nsresult rv
= aHTMLEditor
.CollectEditTargetNodesInExtendedSelectionRanges(
66 arrayOfContents
, EditSubAction::eCreateOrChangeList
,
67 HTMLEditor::CollectNonEditableNodes::No
);
70 "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
71 "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
72 aRv
= EditorBase::ToGenericNSResult(rv
);
76 // Examine list type for nodes in selection.
77 for (const auto& content
: arrayOfContents
) {
78 if (!content
->IsElement()) {
79 mIsOtherContentSelected
= true;
80 } else if (content
->IsHTMLElement(nsGkAtoms::ul
)) {
81 mIsULElementSelected
= true;
82 } else if (content
->IsHTMLElement(nsGkAtoms::ol
)) {
83 mIsOLElementSelected
= true;
84 } else if (content
->IsHTMLElement(nsGkAtoms::li
)) {
85 if (dom::Element
* parent
= content
->GetParentElement()) {
86 if (parent
->IsHTMLElement(nsGkAtoms::ul
)) {
87 mIsULElementSelected
= true;
88 } else if (parent
->IsHTMLElement(nsGkAtoms::ol
)) {
89 mIsOLElementSelected
= true;
92 } else if (content
->IsAnyOfHTMLElements(nsGkAtoms::dl
, nsGkAtoms::dt
,
94 mIsDLElementSelected
= true;
96 mIsOtherContentSelected
= true;
99 if (mIsULElementSelected
&& mIsOLElementSelected
&& mIsDLElementSelected
&&
100 mIsOtherContentSelected
) {
106 /*****************************************************************************
107 * ListItemElementSelectionState
108 ****************************************************************************/
110 ListItemElementSelectionState::ListItemElementSelectionState(
111 HTMLEditor
& aHTMLEditor
, ErrorResult
& aRv
) {
112 MOZ_ASSERT(!aRv
.Failed());
114 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
115 aRv
.Throw(NS_ERROR_EDITOR_DESTROYED
);
119 // XXX Should we create another constructor which won't create
120 // AutoEditActionDataSetter? Or should we create another
121 // AutoEditActionDataSetter which won't nest edit action?
122 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
123 EditAction::eNotEditing
);
124 if (NS_WARN_IF(!editActionData
.CanHandle())) {
125 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
129 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
130 nsresult rv
= aHTMLEditor
.CollectEditTargetNodesInExtendedSelectionRanges(
131 arrayOfContents
, EditSubAction::eCreateOrChangeList
,
132 HTMLEditor::CollectNonEditableNodes::No
);
135 "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
136 "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
137 aRv
= EditorBase::ToGenericNSResult(rv
);
141 // examine list type for nodes in selection
142 for (const auto& content
: arrayOfContents
) {
143 if (!content
->IsElement()) {
144 mIsOtherElementSelected
= true;
145 } else if (content
->IsAnyOfHTMLElements(nsGkAtoms::ul
, nsGkAtoms::ol
,
147 mIsLIElementSelected
= true;
148 } else if (content
->IsHTMLElement(nsGkAtoms::dt
)) {
149 mIsDTElementSelected
= true;
150 } else if (content
->IsHTMLElement(nsGkAtoms::dd
)) {
151 mIsDDElementSelected
= true;
152 } else if (content
->IsHTMLElement(nsGkAtoms::dl
)) {
153 if (mIsDTElementSelected
&& mIsDDElementSelected
) {
156 // need to look inside dl and see which types of items it has
157 DefinitionListItemScanner
scanner(*content
->AsElement());
158 mIsDTElementSelected
|= scanner
.DTElementFound();
159 mIsDDElementSelected
|= scanner
.DDElementFound();
161 mIsOtherElementSelected
= true;
164 if (mIsLIElementSelected
&& mIsDTElementSelected
&& mIsDDElementSelected
&&
165 mIsOtherElementSelected
) {
171 /*****************************************************************************
172 * AlignStateAtSelection
173 ****************************************************************************/
175 AlignStateAtSelection::AlignStateAtSelection(HTMLEditor
& aHTMLEditor
,
177 MOZ_ASSERT(!aRv
.Failed());
179 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
180 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
184 // XXX Should we create another constructor which won't create
185 // AutoEditActionDataSetter? Or should we create another
186 // AutoEditActionDataSetter which won't nest edit action?
187 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
188 EditAction::eNotEditing
);
189 if (NS_WARN_IF(!editActionData
.CanHandle())) {
190 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
194 if (aHTMLEditor
.IsSelectionRangeContainerNotContent()) {
195 NS_WARNING("Some selection containers are not content node, but ignored");
199 // For now, just return first alignment. We don't check if it's mixed.
200 // This is for efficiency given that our current UI doesn't care if it's
202 // cmanske: NOT TRUE! We would like to pay attention to mixed state in
203 // [Format] -> [Align] submenu!
205 // This routine assumes that alignment is done ONLY by `<div>` elements
206 // if aHTMLEditor is not in CSS mode.
208 if (NS_WARN_IF(!aHTMLEditor
.GetRoot())) {
209 aRv
.Throw(NS_ERROR_FAILURE
);
213 OwningNonNull
<dom::Element
> bodyOrDocumentElement
= *aHTMLEditor
.GetRoot();
214 EditorRawDOMPoint
atBodyOrDocumentElement(bodyOrDocumentElement
);
216 const nsRange
* firstRange
= aHTMLEditor
.SelectionRefPtr()->GetRangeAt(0);
217 mFoundSelectionRanges
= !!firstRange
;
218 if (!mFoundSelectionRanges
) {
219 NS_WARNING("There was no selection range");
220 aRv
.Throw(NS_ERROR_FAILURE
);
223 EditorRawDOMPoint
atStartOfSelection(firstRange
->StartRef());
224 if (NS_WARN_IF(!atStartOfSelection
.IsSet())) {
225 aRv
.Throw(NS_ERROR_FAILURE
);
228 MOZ_ASSERT(atStartOfSelection
.IsSetAndValid());
230 nsIContent
* editTargetContent
= nullptr;
231 // If selection is collapsed or in a text node, take the container.
232 if (aHTMLEditor
.SelectionRefPtr()->IsCollapsed() ||
233 atStartOfSelection
.IsInTextNode()) {
234 editTargetContent
= atStartOfSelection
.GetContainerAsContent();
235 if (NS_WARN_IF(!editTargetContent
)) {
236 aRv
.Throw(NS_ERROR_FAILURE
);
240 // If selection container is the `<body>` element which is set to
241 // `HTMLDocument.body`, take first editable node in it.
242 // XXX Why don't we just compare `atStartOfSelection.GetChild()` and
243 // `bodyOrDocumentElement`? Then, we can avoid computing the
245 else if (atStartOfSelection
.IsContainerHTMLElement(nsGkAtoms::html
) &&
246 atBodyOrDocumentElement
.IsSet() &&
247 atStartOfSelection
.Offset() == atBodyOrDocumentElement
.Offset()) {
248 editTargetContent
= aHTMLEditor
.GetNextEditableNode(atStartOfSelection
);
249 if (NS_WARN_IF(!editTargetContent
)) {
250 aRv
.Throw(NS_ERROR_FAILURE
);
254 // Otherwise, use first selected node.
255 // XXX Only for retreiving it, the following block treats all selected
256 // ranges. `HTMLEditor` should have
257 // `GetFirstSelectionRangeExtendedToHardLineStartAndEnd()`.
259 AutoTArray
<RefPtr
<nsRange
>, 4> arrayOfRanges
;
260 aHTMLEditor
.GetSelectionRangesExtendedToHardLineStartAndEnd(
261 arrayOfRanges
, EditSubAction::eSetOrClearAlignment
);
263 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
264 nsresult rv
= aHTMLEditor
.CollectEditTargetNodes(
265 arrayOfRanges
, arrayOfContents
, EditSubAction::eSetOrClearAlignment
,
266 HTMLEditor::CollectNonEditableNodes::Yes
);
269 "HTMLEditor::CollectEditTargetNodes(eSetOrClearAlignment, "
270 "CollectNonEditableNodes::Yes) failed");
271 aRv
.Throw(NS_ERROR_FAILURE
);
274 if (arrayOfContents
.IsEmpty()) {
276 "HTMLEditor::CollectEditTargetNodes(eSetOrClearAlignment, "
277 "CollectNonEditableNodes::Yes) returned no contents");
278 aRv
.Throw(NS_ERROR_FAILURE
);
281 editTargetContent
= arrayOfContents
[0];
284 RefPtr
<dom::Element
> blockElementAtEditTarget
=
285 HTMLEditUtils::GetInclusiveAncestorBlockElement(*editTargetContent
);
286 if (NS_WARN_IF(!blockElementAtEditTarget
)) {
287 aRv
.Throw(NS_ERROR_FAILURE
);
291 if (aHTMLEditor
.IsCSSEnabled() &&
292 CSSEditUtils::IsCSSEditableProperty(blockElementAtEditTarget
, nullptr,
294 // We are in CSS mode and we know how to align this element with CSS
296 // Let's get the value(s) of text-align or margin-left/margin-right
297 DebugOnly
<nsresult
> rvIgnored
=
298 CSSEditUtils::GetComputedCSSEquivalentToHTMLInlineStyleSet(
299 *blockElementAtEditTarget
, nullptr, nsGkAtoms::align
, value
);
300 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
301 aRv
.Throw(NS_ERROR_EDITOR_DESTROYED
);
304 NS_WARNING_ASSERTION(
305 NS_SUCCEEDED(rvIgnored
),
306 "CSSEditUtils::GetComputedCSSEquivalentToHTMLInlineStyleSet(nsGkAtoms::"
308 "eComputed) failed, but ignored");
309 if (value
.EqualsLiteral("center") || value
.EqualsLiteral("-moz-center") ||
310 value
.EqualsLiteral("auto auto")) {
311 mFirstAlign
= nsIHTMLEditor::eCenter
;
314 if (value
.EqualsLiteral("right") || value
.EqualsLiteral("-moz-right") ||
315 value
.EqualsLiteral("auto 0px")) {
316 mFirstAlign
= nsIHTMLEditor::eRight
;
319 if (value
.EqualsLiteral("justify")) {
320 mFirstAlign
= nsIHTMLEditor::eJustify
;
323 // XXX In RTL document, is this expected?
324 mFirstAlign
= nsIHTMLEditor::eLeft
;
328 for (nsIContent
* containerContent
:
329 editTargetContent
->InclusiveAncestorsOfType
<nsIContent
>()) {
330 // If the node is a parent `<table>` element of edit target, let's break
331 // here to materialize the 'inline-block' behaviour of html tables
332 // regarding to text alignment.
333 if (containerContent
!= editTargetContent
&&
334 containerContent
->IsHTMLElement(nsGkAtoms::table
)) {
338 if (CSSEditUtils::IsCSSEditableProperty(containerContent
, nullptr,
341 DebugOnly
<nsresult
> rvIgnored
= CSSEditUtils::GetSpecifiedProperty(
342 *containerContent
, *nsGkAtoms::textAlign
, value
);
343 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
344 "CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::"
345 "textAlign) failed, but ignored");
346 if (!value
.IsEmpty()) {
347 if (value
.EqualsLiteral("center")) {
348 mFirstAlign
= nsIHTMLEditor::eCenter
;
351 if (value
.EqualsLiteral("right")) {
352 mFirstAlign
= nsIHTMLEditor::eRight
;
355 if (value
.EqualsLiteral("justify")) {
356 mFirstAlign
= nsIHTMLEditor::eJustify
;
359 if (value
.EqualsLiteral("left")) {
360 mFirstAlign
= nsIHTMLEditor::eLeft
;
364 // text-align: start and end aren't supported yet
368 if (!HTMLEditUtils::SupportsAlignAttr(*containerContent
)) {
372 nsAutoString alignAttributeValue
;
373 containerContent
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::align
,
374 alignAttributeValue
);
375 if (alignAttributeValue
.IsEmpty()) {
379 if (alignAttributeValue
.LowerCaseEqualsASCII("center")) {
380 mFirstAlign
= nsIHTMLEditor::eCenter
;
383 if (alignAttributeValue
.LowerCaseEqualsASCII("right")) {
384 mFirstAlign
= nsIHTMLEditor::eRight
;
387 // XXX This is odd case. `<div align="justify">` is not in any standards.
388 if (alignAttributeValue
.LowerCaseEqualsASCII("justify")) {
389 mFirstAlign
= nsIHTMLEditor::eJustify
;
392 // XXX In RTL document, is this expected?
393 mFirstAlign
= nsIHTMLEditor::eLeft
;
398 /*****************************************************************************
399 * ParagraphStateAtSelection
400 ****************************************************************************/
402 ParagraphStateAtSelection::ParagraphStateAtSelection(HTMLEditor
& aHTMLEditor
,
404 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
405 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
409 // XXX Should we create another constructor which won't create
410 // AutoEditActionDataSetter? Or should we create another
411 // AutoEditActionDataSetter which won't nest edit action?
412 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
413 EditAction::eNotEditing
);
414 if (NS_WARN_IF(!editActionData
.CanHandle())) {
415 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
419 if (aHTMLEditor
.IsSelectionRangeContainerNotContent()) {
420 NS_WARNING("Some selection containers are not content node, but ignored");
424 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
426 CollectEditableFormatNodesInSelection(aHTMLEditor
, arrayOfContents
);
429 "ParagraphStateAtSelection::CollectEditableFormatNodesInSelection() "
435 // We need to append descendant format block if block nodes are not format
436 // block. This is so we only have to look "up" the hierarchy to find
437 // format nodes, instead of both up and down.
438 for (int32_t i
= arrayOfContents
.Length() - 1; i
>= 0; i
--) {
439 auto& content
= arrayOfContents
[i
];
441 if (HTMLEditUtils::IsBlockElement(content
) &&
442 !HTMLEditUtils::IsFormatNode(content
)) {
443 // XXX This RemoveObject() call has already been commented out and
444 // the above comment explained we're trying to replace non-format
445 // block nodes in the array. According to the following blocks and
446 // `AppendDescendantFormatNodesAndFirstInlineNode()`, replacing
447 // non-format block with descendants format blocks makes sense.
448 // arrayOfContents.RemoveObject(node);
449 ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
450 arrayOfContents
, *content
->AsElement());
454 // We might have an empty node list. if so, find selection parent
455 // and put that on the list
456 if (arrayOfContents
.IsEmpty()) {
457 EditorRawDOMPoint
atCaret(
458 EditorBase::GetStartPoint(*aHTMLEditor
.SelectionRefPtr()));
459 if (NS_WARN_IF(!atCaret
.IsSet())) {
460 aRv
.Throw(NS_ERROR_FAILURE
);
463 nsIContent
* content
= atCaret
.GetContainerAsContent();
464 if (NS_WARN_IF(!content
)) {
465 aRv
.Throw(NS_ERROR_FAILURE
);
468 arrayOfContents
.AppendElement(*content
);
471 dom::Element
* bodyOrDocumentElement
= aHTMLEditor
.GetRoot();
472 if (NS_WARN_IF(!bodyOrDocumentElement
)) {
473 aRv
.Throw(NS_ERROR_FAILURE
);
477 for (auto& content
: Reversed(arrayOfContents
)) {
478 nsAtom
* paragraphStateOfNode
= nsGkAtoms::_empty
;
479 if (HTMLEditUtils::IsFormatNode(content
)) {
480 MOZ_ASSERT(content
->NodeInfo()->NameAtom());
481 paragraphStateOfNode
= content
->NodeInfo()->NameAtom();
483 // Ignore non-format block node since its children have been appended
484 // the list above so that we'll handle this descendants later.
485 else if (HTMLEditUtils::IsBlockElement(content
)) {
488 // If we meet an inline node, let's get its parent format.
490 for (nsINode
* parentNode
= content
->GetParentNode(); parentNode
;
491 parentNode
= parentNode
->GetParentNode()) {
492 // If we reach `HTMLDocument.body` or `Document.documentElement`,
493 // there is no format.
494 if (parentNode
== bodyOrDocumentElement
) {
497 if (HTMLEditUtils::IsFormatNode(parentNode
)) {
498 MOZ_ASSERT(parentNode
->NodeInfo()->NameAtom());
499 paragraphStateOfNode
= parentNode
->NodeInfo()->NameAtom();
505 // if this is the first node, we've found, remember it as the format
506 if (!mFirstParagraphState
) {
507 mFirstParagraphState
= paragraphStateOfNode
;
510 // else make sure it matches previously found format
511 if (mFirstParagraphState
!= paragraphStateOfNode
) {
519 void ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
520 nsTArray
<OwningNonNull
<nsIContent
>>& aArrayOfContents
,
521 dom::Element
& aNonFormatBlockElement
) {
522 MOZ_ASSERT(HTMLEditUtils::IsBlockElement(aNonFormatBlockElement
));
523 MOZ_ASSERT(!HTMLEditUtils::IsFormatNode(&aNonFormatBlockElement
));
525 // We only need to place any one inline inside this node onto
526 // the list. They are all the same for purposes of determining
527 // paragraph style. We use foundInline to track this as we are
528 // going through the children in the loop below.
529 bool foundInline
= false;
530 for (nsIContent
* childContent
= aNonFormatBlockElement
.GetFirstChild();
531 childContent
; childContent
= childContent
->GetNextSibling()) {
532 bool isBlock
= HTMLEditUtils::IsBlockElement(*childContent
);
533 bool isFormat
= HTMLEditUtils::IsFormatNode(childContent
);
534 // If the child is a non-format block element, let's check its children
536 if (isBlock
&& !isFormat
) {
537 ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
538 aArrayOfContents
, *childContent
->AsElement());
542 // If it's a format block, append it.
544 aArrayOfContents
.AppendElement(*childContent
);
548 MOZ_ASSERT(!isBlock
);
550 // If we haven't found inline node, append only this first inline node.
551 // XXX I think that this makes sense if caller of this removes
552 // aNonFormatBlockElement from aArrayOfContents because the last loop
553 // of the constructor can check parent format block with
554 // aNonFormatBlockElement.
557 aArrayOfContents
.AppendElement(*childContent
);
564 nsresult
ParagraphStateAtSelection::CollectEditableFormatNodesInSelection(
565 HTMLEditor
& aHTMLEditor
,
566 nsTArray
<OwningNonNull
<nsIContent
>>& aArrayOfContents
) {
567 nsresult rv
= aHTMLEditor
.CollectEditTargetNodesInExtendedSelectionRanges(
568 aArrayOfContents
, EditSubAction::eCreateOrRemoveBlock
,
569 HTMLEditor::CollectNonEditableNodes::Yes
);
572 "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
573 "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed");
577 // Pre-process our list of nodes
578 for (int32_t i
= aArrayOfContents
.Length() - 1; i
>= 0; i
--) {
579 OwningNonNull
<nsIContent
> content
= aArrayOfContents
[i
];
581 // Remove all non-editable nodes. Leave them be.
582 if (!EditorUtils::IsEditableContent(content
, EditorType::HTML
)) {
583 aArrayOfContents
.RemoveElementAt(i
);
587 // Scan for table elements. If we find table elements other than table,
588 // replace it with a list of any editable non-table content. Ditto for
590 if (HTMLEditUtils::IsAnyTableElement(content
) ||
591 HTMLEditUtils::IsAnyListElement(content
) ||
592 HTMLEditUtils::IsListItem(content
)) {
593 aArrayOfContents
.RemoveElementAt(i
);
594 aHTMLEditor
.CollectChildren(content
, aArrayOfContents
, i
,
595 HTMLEditor::CollectListChildren::Yes
,
596 HTMLEditor::CollectTableChildren::Yes
,
597 HTMLEditor::CollectNonEditableNodes::Yes
);
603 } // namespace mozilla