From 4be6e924dc1911bd02590e1163a4be6612900e3a Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Tue, 17 Jan 2023 05:26:14 +0000 Subject: [PATCH] Bug 1807829 - Make `HTMLEditor::CreateStyleForInsertText` use same logic as `HTMLEditor::SetInlinePropertiesAsSubAction` to apply new style r=m_kato The problem in Yahoo mail is, when you apply new font size to: ```
{}
``` then, when you type text, you'll get: ```
abc
``` because `HTMLEditor::CreateStyleForInsertText` does not check ancestors to apply new style to empty text node which is created by the method for placeholder. Of course, the other browsers update the existing `` instead and we should follow it. However, there are 3 problems: 1. Our editor may adjust insertion point to nearest editable text node if caret in empty inline elements. 2. Our editor supports increase/decrease font size which have been removed from `Document.execCommand`, but Thunderbird keeps using it, and that's implemented with an independent method. 3. Our style editor code does not support applying style to collapsed selection. Therefore, this patch does: 1. keep create empty text node to apply new style at specific point. 2. give up to use new path if there is preserved font size increment/decrement. 3. separate `HTMLEditor::SetInlinePropertiesAsSubAction` and add new path for applying style to collapsed range. Then, `HTMLEditor::CreateStyleForInsertText` can use `HTMLEditor::SetInlinePropertiesAsSubAction` which supports handling new styles with existing ancestors. Differential Revision: https://phabricator.services.mozilla.com/D166416 --- editor/libeditor/HTMLEditSubActionHandler.cpp | 95 +++++-- editor/libeditor/HTMLEditUtils.cpp | 48 +++- editor/libeditor/HTMLEditUtils.h | 8 + editor/libeditor/HTMLEditor.h | 14 +- editor/libeditor/HTMLEditorNestedClasses.h | 29 +- editor/libeditor/HTMLStyleEditor.cpp | 269 ++++++++++++++---- editor/libeditor/PendingStyles.cpp | 15 + editor/libeditor/PendingStyles.h | 7 + .../tests/editing/data/multitest.js | 13 + 9 files changed, 405 insertions(+), 93 deletions(-) diff --git a/editor/libeditor/HTMLEditSubActionHandler.cpp b/editor/libeditor/HTMLEditSubActionHandler.cpp index 0db55e292669..78850dc5bc92 100644 --- a/editor/libeditor/HTMLEditSubActionHandler.cpp +++ b/editor/libeditor/HTMLEditSubActionHandler.cpp @@ -1094,7 +1094,7 @@ Result HTMLEditor::HandleInsertText( // for every property that is set, insert a new inline style node Result setStyleResult = - CreateStyleForInsertText(pointToInsert); + CreateStyleForInsertText(pointToInsert, *editingHost); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); @@ -2258,7 +2258,7 @@ Result HTMLEditor::HandleInsertLinefeed( // should be merged when we fix bug 92921. Result setStyleResult = - CreateStyleForInsertText(aPointToBreak); + CreateStyleForInsertText(aPointToBreak, aEditingHost); if (MOZ_UNLIKELY(setStyleResult.isErr())) { NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed"); return setStyleResult.propagateErr(); @@ -6078,7 +6078,7 @@ Result HTMLEditor::ChangeListElementType( } Result HTMLEditor::CreateStyleForInsertText( - const EditorDOMPoint& aPointToInsertText) { + const EditorDOMPoint& aPointToInsertText, const Element& aEditingHost) { MOZ_ASSERT(IsEditActionDataAvailable()); MOZ_ASSERT(aPointToInsertText.IsSetAndValid()); MOZ_ASSERT(mPendingStylesToApplyToNewContent); @@ -6116,9 +6116,17 @@ Result HTMLEditor::CreateStyleForInsertText( // then process setting any styles const int32_t relFontSize = mPendingStylesToApplyToNewContent->TakeRelativeFontSize(); - pendingStyle = mPendingStylesToApplyToNewContent->TakePreservedStyle(); + AutoTArray stylesToSet; + mPendingStylesToApplyToNewContent->TakeAllPreservedStyles(stylesToSet); + if (stylesToSet.IsEmpty() && !relFontSize) { + return pointToPutCaret; + } - if (pendingStyle || relFontSize) { + // We're in chrome, e.g., the email composer of Thunderbird, and there is + // relative font size changes, we need to keep using legacy path until we port + // IncrementOrDecrementFontSizeAsSubAction() to work with + // AutoInlineStyleSetter. + if (relFontSize) { // we have at least one style to add; make a new text node to insert style // nodes above. EditorDOMPoint pointToInsertTextNode(pointToPutCaret); @@ -6164,34 +6172,26 @@ Result HTMLEditor::CreateStyleForInsertText( insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion(); pointToPutCaret.Set(newEmptyTextNode, 0u); - if (relFontSize) { - HTMLEditor::FontSize incrementOrDecrement = - relFontSize > 0 ? HTMLEditor::FontSize::incr - : HTMLEditor::FontSize::decr; - for ([[maybe_unused]] uint32_t j : IntegerRange(Abs(relFontSize))) { - Result - wrapTextInBigOrSmallElementResult = SetFontSizeOnTextNode( - *newEmptyTextNode, 0, UINT32_MAX, incrementOrDecrement); - if (MOZ_UNLIKELY(wrapTextInBigOrSmallElementResult.isErr())) { - NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed"); - return wrapTextInBigOrSmallElementResult.propagateErr(); - } - // We don't need to update here because we'll suggest caret position - // which is computed above. - MOZ_ASSERT(pointToPutCaret.IsSet()); - wrapTextInBigOrSmallElementResult.inspect() - .IgnoreCaretPointSuggestion(); + HTMLEditor::FontSize incrementOrDecrement = + relFontSize > 0 ? HTMLEditor::FontSize::incr + : HTMLEditor::FontSize::decr; + for ([[maybe_unused]] uint32_t j : IntegerRange(Abs(relFontSize))) { + Result wrapTextInBigOrSmallElementResult = + SetFontSizeOnTextNode(*newEmptyTextNode, 0, UINT32_MAX, + incrementOrDecrement); + if (MOZ_UNLIKELY(wrapTextInBigOrSmallElementResult.isErr())) { + NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed"); + return wrapTextInBigOrSmallElementResult.propagateErr(); } + // We don't need to update here because we'll suggest caret position + // which is computed above. + MOZ_ASSERT(pointToPutCaret.IsSet()); + wrapTextInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion(); } - while (pendingStyle) { - AutoInlineStyleSetter inlineStyleSetter( - pendingStyle->GetAttribute() - ? EditorInlineStyleAndValue( - *pendingStyle->GetTag(), *pendingStyle->GetAttribute(), - pendingStyle->AttributeValueOrCSSValueRef()) - : EditorInlineStyleAndValue(*pendingStyle->GetTag())); - // MOZ_KnownLive(...ContainerAs()) because pointToPutCaret() + for (const EditorInlineStyleAndValue& styleToSet : stylesToSet) { + AutoInlineStyleSetter inlineStyleSetter(styleToSet); + // MOZ_KnownLive(...ContainerAs()) because pointToPutCaret // grabs the result. Result setStyleResult = inlineStyleSetter.ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( @@ -6204,10 +6204,43 @@ Result HTMLEditor::CreateStyleForInsertText( // is computed above. MOZ_ASSERT(pointToPutCaret.IsSet()); setStyleResult.unwrap().IgnoreCaretPointSuggestion(); - pendingStyle = mPendingStylesToApplyToNewContent->TakePreservedStyle(); } + return pointToPutCaret; } + // If we have preserved commands except relative font style changes, we can + // use inline style setting code which reuse ancestors better. + AutoRangeArray ranges(pointToPutCaret); + if (MOZ_UNLIKELY(ranges.Ranges().IsEmpty())) { + NS_WARNING("AutoRangeArray::AutoRangeArray() failed"); + return Err(NS_ERROR_FAILURE); + } + nsresult rv = + SetInlinePropertiesAroundRanges(ranges, stylesToSet, aEditingHost); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed"); + return Err(rv); + } + if (NS_WARN_IF(ranges.Ranges().IsEmpty())) { + return Err(NS_ERROR_FAILURE); + } + // Now `ranges` selects new styled contents and the range may not be + // collapsed. We should use the deepest editable start point of the range + // to insert text. + nsINode* container = ranges.FirstRangeRef()->GetStartContainer(); + if (MOZ_UNLIKELY(!container->IsContent())) { + container = ranges.FirstRangeRef()->GetChildAtStartOffset(); + if (MOZ_UNLIKELY(!container)) { + NS_WARNING("How did we get lost insertion point?"); + return Err(NS_ERROR_FAILURE); + } + } + pointToPutCaret = + HTMLEditUtils::GetDeepestEditableStartPointOf( + *container->AsContent()); + if (NS_WARN_IF(!pointToPutCaret.IsSet())) { + return Err(NS_ERROR_FAILURE); + } return pointToPutCaret; } diff --git a/editor/libeditor/HTMLEditUtils.cpp b/editor/libeditor/HTMLEditUtils.cpp index 41d887dfd007..4e40faae8d95 100644 --- a/editor/libeditor/HTMLEditUtils.cpp +++ b/editor/libeditor/HTMLEditUtils.cpp @@ -106,6 +106,15 @@ template EditorRawDOMPoint HTMLEditUtils::GetBetterInsertionPointFor( const nsIContent& aContentToInsert, const EditorDOMPoint& aPointToInsert, const Element& aEditingHost); +template EditorDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText( + const EditorDOMPoint& aPoint, const Element& aEditingHost); +template EditorDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText( + const EditorRawDOMPoint& aPoint, const Element& aEditingHost); +template EditorRawDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText( + const EditorDOMPoint& aPoint, const Element& aEditingHost); +template EditorRawDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText( + const EditorRawDOMPoint& aPoint, const Element& aEditingHost); + template Result HTMLEditUtils::ComputePointToPutCaretInElementIfOutside( const Element& aElement, const EditorDOMPoint& aCurrentPoint); @@ -2034,7 +2043,44 @@ EditorDOMPointType HTMLEditUtils::GetBetterInsertionPointFor( .template PointAfterContent(); } -// static +// static +template +EditorDOMPointType HTMLEditUtils::GetBetterCaretPositionToInsertText( + const EditorDOMPointTypeInput& aPoint, const Element& aEditingHost) { + MOZ_ASSERT(aPoint.IsSetAndValid()); + MOZ_ASSERT( + aPoint.GetContainer()->IsInclusiveFlatTreeDescendantOf(&aEditingHost)); + + if (aPoint.IsInTextNode()) { + return aPoint.template To(); + } + if (!aPoint.IsEndOfContainer() && aPoint.GetChild() && + aPoint.GetChild()->IsText()) { + return EditorDOMPointType(aPoint.GetChild(), 0u); + } + if (aPoint.IsEndOfContainer()) { + WSRunScanner scanner(&aEditingHost, aPoint); + WSScanResult previousThing = + scanner.ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPoint); + if (previousThing.InVisibleOrCollapsibleCharacters()) { + return EditorDOMPointType::AtEndOf(*previousThing.TextPtr()); + } + } + if (HTMLEditUtils::CanNodeContain(*aPoint.GetContainer(), + *nsGkAtoms::textTagName)) { + return aPoint.template To(); + } + if (MOZ_UNLIKELY(aPoint.GetContainer() == &aEditingHost || + !aPoint.template GetContainerParentAs() || + !HTMLEditUtils::CanNodeContain( + *aPoint.template ContainerParentAs(), + *nsGkAtoms::textTagName))) { + return EditorDOMPointType(); + } + return aPoint.ParentPoint().template To(); +} + +// static template Result HTMLEditUtils::ComputePointToPutCaretInElementIfOutside( diff --git a/editor/libeditor/HTMLEditUtils.h b/editor/libeditor/HTMLEditUtils.h index af7535296c83..970f835ade46 100644 --- a/editor/libeditor/HTMLEditUtils.h +++ b/editor/libeditor/HTMLEditUtils.h @@ -2001,6 +2001,14 @@ class HTMLEditUtils final { const EditorDOMPointTypeInput& aPointToInsert, const Element& aEditingHost); + /** + * GetBetterCaretPositionToInsertText() returns better point to put caret + * if aPoint is near a text node or in non-container node. + */ + template + static EditorDOMPointType GetBetterCaretPositionToInsertText( + const EditorDOMPointTypeInput& aPoint, const Element& aEditingHost); + /** * ComputePointToPutCaretInElementIfOutside() returns a good point in aElement * to put caret if aCurrentPoint is outside of aElement. diff --git a/editor/libeditor/HTMLEditor.h b/editor/libeditor/HTMLEditor.h index 2b85256e2499..ca1584101268 100644 --- a/editor/libeditor/HTMLEditor.h +++ b/editor/libeditor/HTMLEditor.h @@ -1079,10 +1079,12 @@ class HTMLEditor final : public EditorBase, * PendingStyles to proper element node. * * @param aPointToInsertText The point to insert text. + * @param aEditingHost The editing host. * @return A suggest point to put caret or unset point. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result - CreateStyleForInsertText(const EditorDOMPoint& aPointToInsertText); + CreateStyleForInsertText(const EditorDOMPoint& aPointToInsertText, + const Element& aEditingHost); /** * GetMostDistantAncestorMailCiteElement() returns most-ancestor mail cite @@ -3266,6 +3268,16 @@ class HTMLEditor final : public EditorBase, [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetInlinePropertiesAsSubAction( const AutoTArray& aStylesToSet); + /** + * SetInlinePropertiesAroundRanges() applying the styles to the ranges even if + * the ranges are collapsed. + */ + template + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SetInlinePropertiesAroundRanges( + AutoRangeArray& aRanges, + const AutoTArray& aStylesToSet, + const Element& aEditingHost); + /** * RemoveInlinePropertiesAsSubAction() removes specified styles from * mPendingStylesToApplyToNewContent if `Selection` is collapsed. Otherwise, diff --git a/editor/libeditor/HTMLEditorNestedClasses.h b/editor/libeditor/HTMLEditorNestedClasses.h index 8a859899c39b..d71eaa3fdfa0 100644 --- a/editor/libeditor/HTMLEditorNestedClasses.h +++ b/editor/libeditor/HTMLEditorNestedClasses.h @@ -76,7 +76,8 @@ class MOZ_STACK_CLASS HTMLEditor::AutoInlineStyleSetter final * See comments in the definition what this does. */ Result ExtendOrShrinkRangeToApplyTheStyle( - const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange) const; + const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, + const Element& aEditingHost) const; /** * Returns next/previous sibling of aContent or an ancestor of it if it's @@ -87,6 +88,32 @@ class MOZ_STACK_CLASS HTMLEditor::AutoInlineStyleSetter final [[nodiscard]] static nsIContent* GetPreviousEditableInlineContent( const nsIContent& aContent, const nsINode* aLimiter = nullptr); + /** + * GetEmptyTextNodeToApplyNewStyle creates new empty text node to insert + * a new element which will contain newly inserted text or returns existing + * empty text node if aCandidatePointToInsert is around it. + * + * NOTE: Unfortunately, editor does not want to insert text into empty inline + * element in some places (e.g., automatically adjusting caret position to + * nearest text node). Therefore, we need to create new empty text node to + * prepare new styles for inserting text. This method is designed for the + * preparation. + * + * @param aHTMLEditor The editor. + * @param aCandidatePointToInsert The point where the caller wants to + * insert new text. + * @param aEditingHost The editing host. + * @return If this creates new empty text node returns it. + * If this couldn't create new empty text node due to + * the point or aEditingHost cannot have text node, + * returns nullptr. + * Otherwise, returns error. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result, nsresult> + GetEmptyTextNodeToApplyNewStyle(HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aCandidatePointToInsert, + const Element& aEditingHost); + private: [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result ApplyStyle( HTMLEditor& aHTMLEditor, nsIContent& aContent); diff --git a/editor/libeditor/HTMLStyleEditor.cpp b/editor/libeditor/HTMLStyleEditor.cpp index 59e570507fe2..8fd1e7b2d715 100644 --- a/editor/libeditor/HTMLStyleEditor.cpp +++ b/editor/libeditor/HTMLStyleEditor.cpp @@ -15,8 +15,8 @@ #include "HTMLEditUtils.h" #include "PendingStyles.h" #include "SelectionState.h" +#include "WSRunObject.h" -#include "ErrorList.h" #include "mozilla/Assertions.h" #include "mozilla/ContentIterator.h" #include "mozilla/EditorForwards.h" @@ -28,29 +28,29 @@ #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/Text.h" + #include "nsAString.h" +#include "nsAtom.h" #include "nsAttrName.h" -#include "nsCOMPtr.h" #include "nsCaseTreatment.h" #include "nsComponentManagerUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsGkAtoms.h" -#include "nsAtom.h" #include "nsIContent.h" -#include "nsNameSpaceManager.h" #include "nsINode.h" #include "nsIPrincipal.h" #include "nsISupportsImpl.h" #include "nsLiteralString.h" +#include "nsNameSpaceManager.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsStyledElement.h" #include "nsTArray.h" +#include "nsTextNode.h" #include "nsUnicharUtils.h" -#include "nscore.h" namespace mozilla { @@ -66,6 +66,15 @@ template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( template nsresult HTMLEditor::SetInlinePropertiesAsSubAction( const AutoTArray& aStylesToSet); +template nsresult HTMLEditor::SetInlinePropertiesAroundRanges( + AutoRangeArray& aRanges, + const AutoTArray& aStylesToSet, + const Element& aEditingHost); +template nsresult HTMLEditor::SetInlinePropertiesAroundRanges( + AutoRangeArray& aRanges, + const AutoTArray& aStylesToSet, + const Element& aEditingHost); + nsresult HTMLEditor::SetInlinePropertyAsAction(nsStaticAtom& aProperty, nsStaticAtom* aAttribute, const nsAString& aValue, @@ -229,6 +238,12 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( } } + RefPtr const editingHost = + ComputeEditingHost(LimitInBodyElement::No); + if (NS_WARN_IF(!editingHost)) { + return NS_ERROR_FAILURE; + } + AutoPlaceholderBatch treatAsOneTransaction( *this, ScrollSelectionIntoView::Yes, __FUNCTION__); IgnoredErrorResult ignoredError; @@ -247,23 +262,44 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( AutoTransactionsConserveSelection dontChangeMySelection(*this); AutoRangeArray selectionRanges(SelectionRef()); + nsresult rv = SetInlinePropertiesAroundRanges(selectionRanges, aStylesToSet, + *editingHost); + if (NS_FAILED(rv)) { + NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed"); + return rv; + } + MOZ_ASSERT(!selectionRanges.HasSavedRanges()); + rv = selectionRanges.ApplyTo(SelectionRef()); + if (NS_WARN_IF(Destroyed())) { + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed"); + return rv; +} + +template +nsresult HTMLEditor::SetInlinePropertiesAroundRanges( + AutoRangeArray& aRanges, + const AutoTArray& aStylesToSet, + const Element& aEditingHost) { for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) { if (!StaticPrefs:: - editor_inline_style_range_compatible_with_the_other_browsers()) { - MOZ_ALWAYS_TRUE(selectionRanges.SaveAndTrackRanges(*this)); + editor_inline_style_range_compatible_with_the_other_browsers() && + !aRanges.IsCollapsed()) { + MOZ_ALWAYS_TRUE(aRanges.SaveAndTrackRanges(*this)); } AutoInlineStyleSetter inlineStyleSetter(styleToSet); - for (OwningNonNull& selectionRange : selectionRanges.Ranges()) { + for (OwningNonNull& domRange : aRanges.Ranges()) { inlineStyleSetter.Reset(); const EditorDOMRange range = [&]() { - if (selectionRanges.HasSavedRanges()) { + if (aRanges.HasSavedRanges()) { return EditorDOMRange( GetExtendedRangeWrappingEntirelySelectedElements( - EditorRawDOMRange(selectionRange))); + EditorRawDOMRange(domRange))); } Result rangeOrError = inlineStyleSetter.ExtendOrShrinkRangeToApplyTheStyle( - *this, EditorDOMRange(selectionRange)); + *this, EditorDOMRange(domRange), aEditingHost); if (MOZ_UNLIKELY(rangeOrError.isErr())) { NS_WARNING( "HTMLEditor::ExtendOrShrinkRangeToApplyTheStyle() failed, but " @@ -276,11 +312,42 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( continue; } + // If the range is collapsed, we should insert new element there. + if (range.Collapsed()) { + Result, nsresult> emptyTextNodeOrError = + AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle( + *this, range.StartRef(), aEditingHost); + if (MOZ_UNLIKELY(emptyTextNodeOrError.isErr())) { + NS_WARNING( + "AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle() " + "failed"); + return emptyTextNodeOrError.unwrapErr(); + } + if (MOZ_UNLIKELY(!emptyTextNodeOrError.inspect())) { + continue; // Couldn't insert text node there + } + RefPtr emptyTextNode = emptyTextNodeOrError.unwrap(); + Result caretPointOrError = + inlineStyleSetter + .ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle( + *this, *emptyTextNode); + if (MOZ_UNLIKELY(caretPointOrError.isErr())) { + NS_WARNING( + "AutoInlineStyleSetter::" + "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); + return caretPointOrError.unwrapErr(); + } + DebugOnly rvIgnored = domRange->CollapseTo(emptyTextNode, 0); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsRange::CollapseTo() failed, but ignored"); + continue; + } + // Use const_cast hack here for preventing the others to update the range. AutoTrackDOMRange trackRange(RangeUpdaterRef(), const_cast(&range)); auto UpdateSelectionRange = [&]() MOZ_CAN_RUN_SCRIPT { - if (selectionRanges.HasSavedRanges()) { + if (aRanges.HasSavedRanges()) { return; } // If inlineStyleSetter creates elements or setting styles, we should @@ -303,7 +370,7 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( EditorRawDOMPoint>( *inlineStyleSetter.LastHandledPointRef() .ContainerAs()); - nsresult rv = selectionRange->SetStartAndEnd( + nsresult rv = domRange->SetStartAndEnd( startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary()); if (NS_SUCCEEDED(rv)) { trackRange.StopTracking(); @@ -312,8 +379,8 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( } // Otherwise, use the range computed with the tracking original range. trackRange.FlushAndStopTracking(); - selectionRange->SetStartAndEnd(range.StartRef().ToRawRangeBoundary(), - range.EndRef().ToRawRangeBoundary()); + domRange->SetStartAndEnd(range.StartRef().ToRawRangeBoundary(), + range.EndRef().ToRawRangeBoundary()); }; // If range is in a text node, apply new style simply. @@ -329,8 +396,8 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed"); return wrapTextInStyledElementResult.unwrapErr(); } - // There is AutoTransactionsConserveSelection, so we don't need to - // update selection here. + // The caller should handle the ranges as Selection if necessary, and we + // don't want to update aRanges with this result. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); UpdateSelectionRange(); continue; @@ -386,8 +453,8 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed"); return wrapTextInStyledElementResult.unwrapErr(); } - // There is AutoTransactionsConserveSelection, so we don't need to - // update selection here. + // The caller should handle the ranges as Selection if necessary, and we + // don't want to update aRanges with this result. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); } @@ -404,8 +471,8 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( "ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed"); return pointToPutCaretOrError.unwrapErr(); } - // There is AutoTransactionsConserveSelection, so we don't need to - // update selection here. + // The caller should handle the ranges as Selection if necessary, and we + // don't want to update aRanges with this result. pointToPutCaretOrError.inspect().IgnoreCaretPointSuggestion(); } @@ -424,24 +491,80 @@ nsresult HTMLEditor::SetInlinePropertiesAsSubAction( NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed"); return wrapTextInStyledElementResult.unwrapErr(); } - // There is AutoTransactionsConserveSelection, so we don't need to - // update selection here. + // The caller should handle the ranges as Selection if necessary, and we + // don't want to update aRanges with this result. wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion(); } UpdateSelectionRange(); } - if (selectionRanges.HasSavedRanges()) { - selectionRanges.RestoreFromSavedRanges(); + if (aRanges.HasSavedRanges()) { + aRanges.RestoreFromSavedRanges(); } } + return NS_OK; +} - MOZ_ASSERT(!selectionRanges.HasSavedRanges()); - nsresult rv = selectionRanges.ApplyTo(SelectionRef()); - if (NS_WARN_IF(Destroyed())) { - return NS_ERROR_EDITOR_DESTROYED; +// static +Result, nsresult> +HTMLEditor::AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle( + HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert, + const Element& aEditingHost) { + auto pointToInsertNewText = + HTMLEditUtils::GetBetterCaretPositionToInsertText( + aCandidatePointToInsert, aEditingHost); + if (MOZ_UNLIKELY(!pointToInsertNewText.IsSet())) { + return RefPtr(); // cannot insert text there } - NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed"); - return rv; + auto pointToInsertNewStyleOrError = + [&]() MOZ_CAN_RUN_SCRIPT -> Result { + if (!pointToInsertNewText.IsInTextNode()) { + return pointToInsertNewText; + } + if (!pointToInsertNewText.ContainerAs()->TextDataLength()) { + return pointToInsertNewText; // Use it + } + if (pointToInsertNewText.IsStartOfContainer()) { + return pointToInsertNewText.ParentPoint(); + } + if (pointToInsertNewText.IsEndOfContainer()) { + return EditorDOMPoint::After(*pointToInsertNewText.ContainerAs()); + } + Result splitTextNodeResult = + aHTMLEditor.SplitNodeWithTransaction(pointToInsertNewText); + if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) { + NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed"); + return splitTextNodeResult.propagateErr(); + } + SplitNodeResult unwrappedSplitTextNodeResult = splitTextNodeResult.unwrap(); + unwrappedSplitTextNodeResult.IgnoreCaretPointSuggestion(); + return unwrappedSplitTextNodeResult.AtSplitPoint(); + }(); + if (MOZ_UNLIKELY(pointToInsertNewStyleOrError.isErr())) { + return pointToInsertNewStyleOrError.propagateErr(); + } + + // If we already have empty text node which is available for placeholder in + // new styled element, let's use it. + if (pointToInsertNewStyleOrError.inspect().IsInTextNode()) { + return RefPtr( + pointToInsertNewStyleOrError.inspect().ContainerAs()); + } + + // Otherwise, we need an empty text node to create new inline style. + RefPtr newEmptyTextNode = aHTMLEditor.CreateTextNode(u""_ns); + if (MOZ_UNLIKELY(!newEmptyTextNode)) { + NS_WARNING("EditorBase::CreateTextNode() failed"); + return Err(NS_ERROR_FAILURE); + } + Result insertNewTextNodeResult = + aHTMLEditor.InsertNodeWithTransaction( + *newEmptyTextNode, pointToInsertNewStyleOrError.inspect()); + if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) { + NS_WARNING("EditorBase::InsertNodeWithTransaction() failed"); + return insertNewTextNodeResult.propagateErr(); + } + insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion(); + return newEmptyTextNode; } Result @@ -1646,7 +1769,8 @@ EditorRawDOMRange HTMLEditor::AutoInlineStyleSetter:: Result HTMLEditor::AutoInlineStyleSetter::ExtendOrShrinkRangeToApplyTheStyle( - const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange) const { + const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, + const Element& aEditingHost) const { if (NS_WARN_IF(!aRange.IsPositioned())) { return Err(NS_ERROR_FAILURE); } @@ -1654,43 +1778,70 @@ HTMLEditor::AutoInlineStyleSetter::ExtendOrShrinkRangeToApplyTheStyle( // For avoiding assertion hits in the utility methods, check whether the // range is in same subtree, first. Even if the range crosses a subtree // boundary, it's not a bug of this module. - nsINode* const commonAncestor = aRange.GetClosestCommonInclusiveAncestor(); + nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor(); if (NS_WARN_IF(!commonAncestor)) { return Err(NS_ERROR_FAILURE); } + // If the range does not select only invisible
element, let's extend the + // range to contain the
element. + EditorDOMRange range(aRange); + if (range.EndRef().IsInContentNode()) { + WSScanResult nextContentData = + WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(&aEditingHost, + range.EndRef()); + if (nextContentData.ReachedInvisibleBRElement() && + nextContentData.BRElementPtr()->GetParentElement() && + HTMLEditUtils::IsInlineElement( + *nextContentData.BRElementPtr()->GetParentElement())) { + range.SetEnd(EditorDOMPoint::After(*nextContentData.BRElementPtr())); + MOZ_ASSERT(range.EndRef().IsSet()); + } + } + + // If the range is collapsed, we don't want to replace ancestors unless it's + // in an empty element. + if (range.Collapsed() && range.StartRef().GetContainer()->Length()) { + return EditorRawDOMRange(range); + } + // First, shrink the given range to minimize new style applied contents. // However, we should not shrink the range into entirely selected element. // E.g., if `abc[def]ghi`, shouldn't shrink it as // `abc[def]ghi`. - ContentSubtreeIterator iter; - if (NS_FAILED(iter.Init(aRange.StartRef().ToRawRangeBoundary(), - aRange.EndRef().ToRawRangeBoundary()))) { - NS_WARNING("ContentSubtreeIterator::Init() failed"); - return Err(NS_ERROR_FAILURE); - } - nsIContent* const firstContentEntirelyInRange = - nsIContent::FromNodeOrNull(iter.GetCurrentNode()); - nsIContent* const lastContentEntirelyInRange = [&]() { - iter.Last(); - return nsIContent::FromNodeOrNull(iter.GetCurrentNode()); - }(); + EditorRawDOMPoint startPoint, endPoint; + if (range.Collapsed()) { + startPoint = endPoint = range.StartRef().To(); + } else { + ContentSubtreeIterator iter; + if (NS_FAILED(iter.Init(range.StartRef().ToRawRangeBoundary(), + range.EndRef().ToRawRangeBoundary()))) { + NS_WARNING("ContentSubtreeIterator::Init() failed"); + return Err(NS_ERROR_FAILURE); + } + nsIContent* const firstContentEntirelyInRange = + nsIContent::FromNodeOrNull(iter.GetCurrentNode()); + nsIContent* const lastContentEntirelyInRange = [&]() { + iter.Last(); + return nsIContent::FromNodeOrNull(iter.GetCurrentNode()); + }(); - // Compute the shrunken range boundaries. - EditorRawDOMPoint startPoint = GetShrunkenRangeStart( - aHTMLEditor, aRange, *commonAncestor, firstContentEntirelyInRange); - MOZ_ASSERT(startPoint.IsSet()); - EditorRawDOMPoint endPoint = GetShrunkenRangeEnd( - aHTMLEditor, aRange, *commonAncestor, lastContentEntirelyInRange); - MOZ_ASSERT(endPoint.IsSet()); + // Compute the shrunken range boundaries. + startPoint = GetShrunkenRangeStart(aHTMLEditor, range, *commonAncestor, + firstContentEntirelyInRange); + MOZ_ASSERT(startPoint.IsSet()); + endPoint = GetShrunkenRangeEnd(aHTMLEditor, range, *commonAncestor, + lastContentEntirelyInRange); + MOZ_ASSERT(endPoint.IsSet()); - // If shrunken range is swapped, it could like this case: - // `abc[]def`, starts at very end of a node and ends at - // very start of immediately next node. In this case, we should use - // the original range instead. - if (MOZ_UNLIKELY(!startPoint.EqualsOrIsBefore(endPoint))) { - startPoint = aRange.StartRef().To(); - endPoint = aRange.EndRef().To(); + // If shrunken range is swapped, it could like this case: + // `abc[]def`, starts at very end of a node and ends at + // very start of immediately next node. In this case, we should use + // the original range instead. + if (MOZ_UNLIKELY(!startPoint.EqualsOrIsBefore(endPoint))) { + startPoint = range.StartRef().To(); + endPoint = range.EndRef().To(); + } } // Then, we may need to extend the range to wrap parent inline elements diff --git a/editor/libeditor/PendingStyles.cpp b/editor/libeditor/PendingStyles.cpp index ca1029253068..2c6b713511d1 100644 --- a/editor/libeditor/PendingStyles.cpp +++ b/editor/libeditor/PendingStyles.cpp @@ -408,6 +408,21 @@ void PendingStyles::ClearStyleInternal( aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle)); } +void PendingStyles::TakeAllPreservedStyles( + nsTArray& aOutStylesAndValues) { + aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() + + mPreservingStyles.Length()); + for (UniquePtr preservedStyle = TakePreservedStyle(); + preservedStyle; preservedStyle = TakePreservedStyle()) { + aOutStylesAndValues.AppendElement( + preservedStyle->GetAttribute() + ? EditorInlineStyleAndValue( + *preservedStyle->GetTag(), *preservedStyle->GetAttribute(), + preservedStyle->AttributeValueOrCSSValueRef()) + : EditorInlineStyleAndValue(*preservedStyle->GetTag())); + } +} + /** * TakeRelativeFontSize() hands back relative font value, which is then * cleared out. diff --git a/editor/libeditor/PendingStyles.h b/editor/libeditor/PendingStyles.h index e8c5d01057f6..fcb225b12101 100644 --- a/editor/libeditor/PendingStyles.h +++ b/editor/libeditor/PendingStyles.h @@ -262,6 +262,13 @@ class PendingStyles final { return mPreservingStyles.PopLastElement(); } + /** + * TakeAllPreservedStyles() moves all preserved styles and values to + * aOutStylesAndValues. + */ + void TakeAllPreservedStyles( + nsTArray& aOutStylesAndValues); + /** * TakeRelativeFontSize() hands back relative font value, which is then * cleared out. diff --git a/testing/web-platform/tests/editing/data/multitest.js b/testing/web-platform/tests/editing/data/multitest.js index 135f133ebc34..9948c338b407 100644 --- a/testing/web-platform/tests/editing/data/multitest.js +++ b/testing/web-platform/tests/editing/data/multitest.js @@ -2853,4 +2853,17 @@ var browserTests = [ [true,true,true], {}], +// element should be reused when the font-size is change for new text. +["{}
", + [["stylewithcss","false"],["fontsize","4"],["insertText","a"]], + ["a[]
", + "a[]"], + [true,true,true], + {"fontsize":[false,false,"7",false,false,"4"]}], +["{}
", + [["stylewithcss","true"],["italic",""],["insertText","a"]], + ["a[]
", + "a[]"], + [true,true,true], + {}], ]