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 using EditorType
= EditorUtils::EditorType
;
42 using WalkTreeOption
= HTMLEditUtils::WalkTreeOption
;
44 /*****************************************************************************
45 * ListElementSelectionState
46 ****************************************************************************/
48 ListElementSelectionState::ListElementSelectionState(HTMLEditor
& aHTMLEditor
,
50 MOZ_ASSERT(!aRv
.Failed());
52 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
53 aRv
.Throw(NS_ERROR_EDITOR_DESTROYED
);
57 // XXX Should we create another constructor which won't create
58 // AutoEditActionDataSetter? Or should we create another
59 // AutoEditActionDataSetter which won't nest edit action?
60 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
61 EditAction::eNotEditing
);
62 if (NS_WARN_IF(!editActionData
.CanHandle())) {
63 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
67 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
68 nsresult rv
= aHTMLEditor
.CollectEditTargetNodesInExtendedSelectionRanges(
69 arrayOfContents
, EditSubAction::eCreateOrChangeList
,
70 HTMLEditor::CollectNonEditableNodes::No
);
73 "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
74 "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
75 aRv
= EditorBase::ToGenericNSResult(rv
);
79 // Examine list type for nodes in selection.
80 for (const auto& content
: arrayOfContents
) {
81 if (!content
->IsElement()) {
82 mIsOtherContentSelected
= true;
83 } else if (content
->IsHTMLElement(nsGkAtoms::ul
)) {
84 mIsULElementSelected
= true;
85 } else if (content
->IsHTMLElement(nsGkAtoms::ol
)) {
86 mIsOLElementSelected
= true;
87 } else if (content
->IsHTMLElement(nsGkAtoms::li
)) {
88 if (dom::Element
* parent
= content
->GetParentElement()) {
89 if (parent
->IsHTMLElement(nsGkAtoms::ul
)) {
90 mIsULElementSelected
= true;
91 } else if (parent
->IsHTMLElement(nsGkAtoms::ol
)) {
92 mIsOLElementSelected
= true;
95 } else if (content
->IsAnyOfHTMLElements(nsGkAtoms::dl
, nsGkAtoms::dt
,
97 mIsDLElementSelected
= true;
99 mIsOtherContentSelected
= true;
102 if (mIsULElementSelected
&& mIsOLElementSelected
&& mIsDLElementSelected
&&
103 mIsOtherContentSelected
) {
109 /*****************************************************************************
110 * ListItemElementSelectionState
111 ****************************************************************************/
113 ListItemElementSelectionState::ListItemElementSelectionState(
114 HTMLEditor
& aHTMLEditor
, ErrorResult
& aRv
) {
115 MOZ_ASSERT(!aRv
.Failed());
117 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
118 aRv
.Throw(NS_ERROR_EDITOR_DESTROYED
);
122 // XXX Should we create another constructor which won't create
123 // AutoEditActionDataSetter? Or should we create another
124 // AutoEditActionDataSetter which won't nest edit action?
125 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
126 EditAction::eNotEditing
);
127 if (NS_WARN_IF(!editActionData
.CanHandle())) {
128 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
132 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
133 nsresult rv
= aHTMLEditor
.CollectEditTargetNodesInExtendedSelectionRanges(
134 arrayOfContents
, EditSubAction::eCreateOrChangeList
,
135 HTMLEditor::CollectNonEditableNodes::No
);
138 "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
139 "eCreateOrChangeList, CollectNonEditableNodes::No) failed");
140 aRv
= EditorBase::ToGenericNSResult(rv
);
144 // examine list type for nodes in selection
145 for (const auto& content
: arrayOfContents
) {
146 if (!content
->IsElement()) {
147 mIsOtherElementSelected
= true;
148 } else if (content
->IsAnyOfHTMLElements(nsGkAtoms::ul
, nsGkAtoms::ol
,
150 mIsLIElementSelected
= true;
151 } else if (content
->IsHTMLElement(nsGkAtoms::dt
)) {
152 mIsDTElementSelected
= true;
153 } else if (content
->IsHTMLElement(nsGkAtoms::dd
)) {
154 mIsDDElementSelected
= true;
155 } else if (content
->IsHTMLElement(nsGkAtoms::dl
)) {
156 if (mIsDTElementSelected
&& mIsDDElementSelected
) {
159 // need to look inside dl and see which types of items it has
160 DefinitionListItemScanner
scanner(*content
->AsElement());
161 mIsDTElementSelected
|= scanner
.DTElementFound();
162 mIsDDElementSelected
|= scanner
.DDElementFound();
164 mIsOtherElementSelected
= true;
167 if (mIsLIElementSelected
&& mIsDTElementSelected
&& mIsDDElementSelected
&&
168 mIsOtherElementSelected
) {
174 /*****************************************************************************
175 * AlignStateAtSelection
176 ****************************************************************************/
178 AlignStateAtSelection::AlignStateAtSelection(HTMLEditor
& aHTMLEditor
,
180 MOZ_ASSERT(!aRv
.Failed());
182 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
183 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
187 // XXX Should we create another constructor which won't create
188 // AutoEditActionDataSetter? Or should we create another
189 // AutoEditActionDataSetter which won't nest edit action?
190 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
191 EditAction::eNotEditing
);
192 if (NS_WARN_IF(!editActionData
.CanHandle())) {
193 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
197 if (aHTMLEditor
.IsSelectionRangeContainerNotContent()) {
198 NS_WARNING("Some selection containers are not content node, but ignored");
202 // For now, just return first alignment. We don't check if it's mixed.
203 // This is for efficiency given that our current UI doesn't care if it's
205 // cmanske: NOT TRUE! We would like to pay attention to mixed state in
206 // [Format] -> [Align] submenu!
208 // This routine assumes that alignment is done ONLY by `<div>` elements
209 // if aHTMLEditor is not in CSS mode.
211 if (NS_WARN_IF(!aHTMLEditor
.GetRoot())) {
212 aRv
.Throw(NS_ERROR_FAILURE
);
216 OwningNonNull
<dom::Element
> bodyOrDocumentElement
= *aHTMLEditor
.GetRoot();
217 EditorRawDOMPoint
atBodyOrDocumentElement(bodyOrDocumentElement
);
219 const nsRange
* firstRange
= aHTMLEditor
.SelectionRef().GetRangeAt(0);
220 mFoundSelectionRanges
= !!firstRange
;
221 if (!mFoundSelectionRanges
) {
222 NS_WARNING("There was no selection range");
223 aRv
.Throw(NS_ERROR_FAILURE
);
226 EditorRawDOMPoint
atStartOfSelection(firstRange
->StartRef());
227 if (NS_WARN_IF(!atStartOfSelection
.IsSet())) {
228 aRv
.Throw(NS_ERROR_FAILURE
);
231 MOZ_ASSERT(atStartOfSelection
.IsSetAndValid());
233 nsIContent
* editTargetContent
= nullptr;
234 // If selection is collapsed or in a text node, take the container.
235 if (aHTMLEditor
.SelectionRef().IsCollapsed() ||
236 atStartOfSelection
.IsInTextNode()) {
237 editTargetContent
= atStartOfSelection
.GetContainerAsContent();
238 if (NS_WARN_IF(!editTargetContent
)) {
239 aRv
.Throw(NS_ERROR_FAILURE
);
243 // If selection container is the `<body>` element which is set to
244 // `HTMLDocument.body`, take first editable node in it.
245 // XXX Why don't we just compare `atStartOfSelection.GetChild()` and
246 // `bodyOrDocumentElement`? Then, we can avoid computing the
248 else if (atStartOfSelection
.IsContainerHTMLElement(nsGkAtoms::html
) &&
249 atBodyOrDocumentElement
.IsSet() &&
250 atStartOfSelection
.Offset() == atBodyOrDocumentElement
.Offset()) {
251 editTargetContent
= HTMLEditUtils::GetNextContent(
252 atStartOfSelection
, {WalkTreeOption::IgnoreNonEditableNode
},
253 aHTMLEditor
.ComputeEditingHost());
254 if (NS_WARN_IF(!editTargetContent
)) {
255 aRv
.Throw(NS_ERROR_FAILURE
);
259 // Otherwise, use first selected node.
260 // XXX Only for retreiving it, the following block treats all selected
261 // ranges. `HTMLEditor` should have
262 // `GetFirstSelectionRangeExtendedToHardLineStartAndEnd()`.
264 AutoTArray
<RefPtr
<nsRange
>, 4> arrayOfRanges
;
265 aHTMLEditor
.GetSelectionRangesExtendedToHardLineStartAndEnd(
266 arrayOfRanges
, EditSubAction::eSetOrClearAlignment
);
268 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
269 nsresult rv
= aHTMLEditor
.CollectEditTargetNodes(
270 arrayOfRanges
, arrayOfContents
, EditSubAction::eSetOrClearAlignment
,
271 HTMLEditor::CollectNonEditableNodes::Yes
);
274 "HTMLEditor::CollectEditTargetNodes(eSetOrClearAlignment, "
275 "CollectNonEditableNodes::Yes) failed");
276 aRv
.Throw(NS_ERROR_FAILURE
);
279 if (arrayOfContents
.IsEmpty()) {
281 "HTMLEditor::CollectEditTargetNodes(eSetOrClearAlignment, "
282 "CollectNonEditableNodes::Yes) returned no contents");
283 aRv
.Throw(NS_ERROR_FAILURE
);
286 editTargetContent
= arrayOfContents
[0];
289 const RefPtr
<dom::Element
> maybeNonEditableBlockElement
=
290 HTMLEditUtils::GetInclusiveAncestorElement(
291 *editTargetContent
, HTMLEditUtils::ClosestBlockElement
);
292 if (NS_WARN_IF(!maybeNonEditableBlockElement
)) {
293 aRv
.Throw(NS_ERROR_FAILURE
);
297 if (aHTMLEditor
.IsCSSEnabled() &&
298 CSSEditUtils::IsCSSEditableProperty(maybeNonEditableBlockElement
, nullptr,
300 // We are in CSS mode and we know how to align this element with CSS
302 // Let's get the value(s) of text-align or margin-left/margin-right
303 DebugOnly
<nsresult
> rvIgnored
=
304 CSSEditUtils::GetComputedCSSEquivalentToHTMLInlineStyleSet(
305 *maybeNonEditableBlockElement
, nullptr, nsGkAtoms::align
, value
);
306 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
307 aRv
.Throw(NS_ERROR_EDITOR_DESTROYED
);
310 NS_WARNING_ASSERTION(
311 NS_SUCCEEDED(rvIgnored
),
312 "CSSEditUtils::GetComputedCSSEquivalentToHTMLInlineStyleSet(nsGkAtoms::"
314 "eComputed) failed, but ignored");
315 if (value
.EqualsLiteral("center") || value
.EqualsLiteral("-moz-center") ||
316 value
.EqualsLiteral("auto auto")) {
317 mFirstAlign
= nsIHTMLEditor::eCenter
;
320 if (value
.EqualsLiteral("right") || value
.EqualsLiteral("-moz-right") ||
321 value
.EqualsLiteral("auto 0px")) {
322 mFirstAlign
= nsIHTMLEditor::eRight
;
325 if (value
.EqualsLiteral("justify")) {
326 mFirstAlign
= nsIHTMLEditor::eJustify
;
329 // XXX In RTL document, is this expected?
330 mFirstAlign
= nsIHTMLEditor::eLeft
;
334 for (nsIContent
* containerContent
:
335 editTargetContent
->InclusiveAncestorsOfType
<nsIContent
>()) {
336 // If the node is a parent `<table>` element of edit target, let's break
337 // here to materialize the 'inline-block' behaviour of html tables
338 // regarding to text alignment.
339 if (containerContent
!= editTargetContent
&&
340 containerContent
->IsHTMLElement(nsGkAtoms::table
)) {
344 if (CSSEditUtils::IsCSSEditableProperty(containerContent
, nullptr,
347 DebugOnly
<nsresult
> rvIgnored
= CSSEditUtils::GetSpecifiedProperty(
348 *containerContent
, *nsGkAtoms::textAlign
, value
);
349 NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored
),
350 "CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::"
351 "textAlign) failed, but ignored");
352 if (!value
.IsEmpty()) {
353 if (value
.EqualsLiteral("center")) {
354 mFirstAlign
= nsIHTMLEditor::eCenter
;
357 if (value
.EqualsLiteral("right")) {
358 mFirstAlign
= nsIHTMLEditor::eRight
;
361 if (value
.EqualsLiteral("justify")) {
362 mFirstAlign
= nsIHTMLEditor::eJustify
;
365 if (value
.EqualsLiteral("left")) {
366 mFirstAlign
= nsIHTMLEditor::eLeft
;
370 // text-align: start and end aren't supported yet
374 if (!HTMLEditUtils::SupportsAlignAttr(*containerContent
)) {
378 nsAutoString alignAttributeValue
;
379 containerContent
->AsElement()->GetAttr(kNameSpaceID_None
, nsGkAtoms::align
,
380 alignAttributeValue
);
381 if (alignAttributeValue
.IsEmpty()) {
385 if (alignAttributeValue
.LowerCaseEqualsASCII("center")) {
386 mFirstAlign
= nsIHTMLEditor::eCenter
;
389 if (alignAttributeValue
.LowerCaseEqualsASCII("right")) {
390 mFirstAlign
= nsIHTMLEditor::eRight
;
393 // XXX This is odd case. `<div align="justify">` is not in any standards.
394 if (alignAttributeValue
.LowerCaseEqualsASCII("justify")) {
395 mFirstAlign
= nsIHTMLEditor::eJustify
;
398 // XXX In RTL document, is this expected?
399 mFirstAlign
= nsIHTMLEditor::eLeft
;
404 /*****************************************************************************
405 * ParagraphStateAtSelection
406 ****************************************************************************/
408 ParagraphStateAtSelection::ParagraphStateAtSelection(HTMLEditor
& aHTMLEditor
,
410 if (NS_WARN_IF(aHTMLEditor
.Destroyed())) {
411 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
415 // XXX Should we create another constructor which won't create
416 // AutoEditActionDataSetter? Or should we create another
417 // AutoEditActionDataSetter which won't nest edit action?
418 EditorBase::AutoEditActionDataSetter
editActionData(aHTMLEditor
,
419 EditAction::eNotEditing
);
420 if (NS_WARN_IF(!editActionData
.CanHandle())) {
421 aRv
= EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED
);
425 if (aHTMLEditor
.IsSelectionRangeContainerNotContent()) {
426 NS_WARNING("Some selection containers are not content node, but ignored");
430 AutoTArray
<OwningNonNull
<nsIContent
>, 64> arrayOfContents
;
432 CollectEditableFormatNodesInSelection(aHTMLEditor
, arrayOfContents
);
435 "ParagraphStateAtSelection::CollectEditableFormatNodesInSelection() "
441 // We need to append descendant format block if block nodes are not format
442 // block. This is so we only have to look "up" the hierarchy to find
443 // format nodes, instead of both up and down.
444 for (int32_t i
= arrayOfContents
.Length() - 1; i
>= 0; i
--) {
445 auto& content
= arrayOfContents
[i
];
447 if (HTMLEditUtils::IsBlockElement(content
) &&
448 !HTMLEditUtils::IsFormatNode(content
)) {
449 // XXX This RemoveObject() call has already been commented out and
450 // the above comment explained we're trying to replace non-format
451 // block nodes in the array. According to the following blocks and
452 // `AppendDescendantFormatNodesAndFirstInlineNode()`, replacing
453 // non-format block with descendants format blocks makes sense.
454 // arrayOfContents.RemoveObject(node);
455 ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
456 arrayOfContents
, *content
->AsElement());
460 // We might have an empty node list. if so, find selection parent
461 // and put that on the list
462 if (arrayOfContents
.IsEmpty()) {
464 aHTMLEditor
.GetFirstSelectionStartPoint
<EditorRawDOMPoint
>();
465 if (NS_WARN_IF(!atCaret
.IsSet())) {
466 aRv
.Throw(NS_ERROR_FAILURE
);
469 nsIContent
* content
= atCaret
.GetContainerAsContent();
470 if (NS_WARN_IF(!content
)) {
471 aRv
.Throw(NS_ERROR_FAILURE
);
474 arrayOfContents
.AppendElement(*content
);
477 dom::Element
* bodyOrDocumentElement
= aHTMLEditor
.GetRoot();
478 if (NS_WARN_IF(!bodyOrDocumentElement
)) {
479 aRv
.Throw(NS_ERROR_FAILURE
);
483 for (auto& content
: Reversed(arrayOfContents
)) {
484 nsAtom
* paragraphStateOfNode
= nsGkAtoms::_empty
;
485 if (HTMLEditUtils::IsFormatNode(content
)) {
486 MOZ_ASSERT(content
->NodeInfo()->NameAtom());
487 paragraphStateOfNode
= content
->NodeInfo()->NameAtom();
489 // Ignore non-format block node since its children have been appended
490 // the list above so that we'll handle this descendants later.
491 else if (HTMLEditUtils::IsBlockElement(content
)) {
494 // If we meet an inline node, let's get its parent format.
496 for (nsINode
* parentNode
= content
->GetParentNode(); parentNode
;
497 parentNode
= parentNode
->GetParentNode()) {
498 // If we reach `HTMLDocument.body` or `Document.documentElement`,
499 // there is no format.
500 if (parentNode
== bodyOrDocumentElement
) {
503 if (HTMLEditUtils::IsFormatNode(parentNode
)) {
504 MOZ_ASSERT(parentNode
->NodeInfo()->NameAtom());
505 paragraphStateOfNode
= parentNode
->NodeInfo()->NameAtom();
511 // if this is the first node, we've found, remember it as the format
512 if (!mFirstParagraphState
) {
513 mFirstParagraphState
= paragraphStateOfNode
;
516 // else make sure it matches previously found format
517 if (mFirstParagraphState
!= paragraphStateOfNode
) {
525 void ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
526 nsTArray
<OwningNonNull
<nsIContent
>>& aArrayOfContents
,
527 dom::Element
& aNonFormatBlockElement
) {
528 MOZ_ASSERT(HTMLEditUtils::IsBlockElement(aNonFormatBlockElement
));
529 MOZ_ASSERT(!HTMLEditUtils::IsFormatNode(&aNonFormatBlockElement
));
531 // We only need to place any one inline inside this node onto
532 // the list. They are all the same for purposes of determining
533 // paragraph style. We use foundInline to track this as we are
534 // going through the children in the loop below.
535 bool foundInline
= false;
536 for (nsIContent
* childContent
= aNonFormatBlockElement
.GetFirstChild();
537 childContent
; childContent
= childContent
->GetNextSibling()) {
538 bool isBlock
= HTMLEditUtils::IsBlockElement(*childContent
);
539 bool isFormat
= HTMLEditUtils::IsFormatNode(childContent
);
540 // If the child is a non-format block element, let's check its children
542 if (isBlock
&& !isFormat
) {
543 ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
544 aArrayOfContents
, *childContent
->AsElement());
548 // If it's a format block, append it.
550 aArrayOfContents
.AppendElement(*childContent
);
554 MOZ_ASSERT(!isBlock
);
556 // If we haven't found inline node, append only this first inline node.
557 // XXX I think that this makes sense if caller of this removes
558 // aNonFormatBlockElement from aArrayOfContents because the last loop
559 // of the constructor can check parent format block with
560 // aNonFormatBlockElement.
563 aArrayOfContents
.AppendElement(*childContent
);
570 nsresult
ParagraphStateAtSelection::CollectEditableFormatNodesInSelection(
571 HTMLEditor
& aHTMLEditor
,
572 nsTArray
<OwningNonNull
<nsIContent
>>& aArrayOfContents
) {
573 nsresult rv
= aHTMLEditor
.CollectEditTargetNodesInExtendedSelectionRanges(
574 aArrayOfContents
, EditSubAction::eCreateOrRemoveBlock
,
575 HTMLEditor::CollectNonEditableNodes::Yes
);
578 "HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges("
579 "eCreateOrRemoveBlock, CollectNonEditableNodes::Yes) failed");
583 // Pre-process our list of nodes
584 for (int32_t i
= aArrayOfContents
.Length() - 1; i
>= 0; i
--) {
585 OwningNonNull
<nsIContent
> content
= aArrayOfContents
[i
];
587 // Remove all non-editable nodes. Leave them be.
588 if (!EditorUtils::IsEditableContent(content
, EditorType::HTML
)) {
589 aArrayOfContents
.RemoveElementAt(i
);
593 // Scan for table elements. If we find table elements other than table,
594 // replace it with a list of any editable non-table content. Ditto for
596 if (HTMLEditUtils::IsAnyTableElement(content
) ||
597 HTMLEditUtils::IsAnyListElement(content
) ||
598 HTMLEditUtils::IsListItem(content
)) {
599 aArrayOfContents
.RemoveElementAt(i
);
600 aHTMLEditor
.CollectChildren(content
, aArrayOfContents
, i
,
601 HTMLEditor::CollectListChildren::Yes
,
602 HTMLEditor::CollectTableChildren::Yes
,
603 HTMLEditor::CollectNonEditableNodes::Yes
);
609 } // namespace mozilla