diff options
Diffstat (limited to 'editor/libeditor/PendingStyles.cpp')
-rw-r--r-- | editor/libeditor/PendingStyles.cpp | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/editor/libeditor/PendingStyles.cpp b/editor/libeditor/PendingStyles.cpp new file mode 100644 index 0000000000..12666c5289 --- /dev/null +++ b/editor/libeditor/PendingStyles.cpp @@ -0,0 +1,503 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PendingStyles.h" + +#include <stddef.h> + +#include "EditAction.h" +#include "EditorBase.h" +#include "HTMLEditHelpers.h" // for EditorInlineStyle, EditorInlineStyleAndValue +#include "HTMLEditor.h" +#include "HTMLEditUtils.h" + +#include "mozilla/mozalloc.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/Selection.h" + +#include "nsDebug.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsINode.h" +#include "nsISupports.h" +#include "nsISupportsImpl.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { + +using namespace dom; + +/******************************************************************** + * mozilla::PendingStyle + *******************************************************************/ + +EditorInlineStyle PendingStyle::ToInlineStyle() const { + return mTag ? EditorInlineStyle(*mTag, mAttribute) + : EditorInlineStyle::RemoveAllStyles(); +} + +EditorInlineStyleAndValue PendingStyle::ToInlineStyleAndValue() const { + MOZ_ASSERT(mTag); + return mAttribute ? EditorInlineStyleAndValue(*mTag, *mAttribute, + mAttributeValueOrCSSValue) + : EditorInlineStyleAndValue(*mTag); +} + +/******************************************************************** + * mozilla::PendingStyleCache + *******************************************************************/ + +EditorInlineStyle PendingStyleCache::ToInlineStyle() const { + return EditorInlineStyle(mTag, mAttribute); +} + +/******************************************************************** + * mozilla::PendingStyles + *******************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(PendingStyles) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PendingStyles) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PendingStyles) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +nsresult PendingStyles::UpdateSelState(const HTMLEditor& aHTMLEditor) { + if (!aHTMLEditor.SelectionRef().IsCollapsed()) { + return NS_OK; + } + + mLastSelectionPoint = + aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (!mLastSelectionPoint.IsSet()) { + return NS_ERROR_FAILURE; + } + // We need to store only offset because referring child may be removed by + // we'll check the point later. + AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint); + return NS_OK; +} + +void PendingStyles::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) { + MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown || + aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp); + bool& eventFiredInLinkElement = + aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown + ? mMouseDownFiredInLinkElement + : mMouseUpFiredInLinkElement; + eventFiredInLinkElement = false; + if (aMouseDownOrUpEvent.DefaultPrevented()) { + return; + } + // If mouse button is down or up in a link element, we shouldn't unlink + // it when we get a notification of selection change. + EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget(); + if (NS_WARN_IF(!target)) { + return; + } + nsIContent* targetContent = nsIContent::FromEventTarget(target); + if (NS_WARN_IF(!targetContent)) { + return; + } + eventFiredInLinkElement = + HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent); +} + +void PendingStyles::PreHandleSelectionChangeCommand(Command aCommand) { + mLastSelectionCommand = aCommand; +} + +void PendingStyles::PostHandleSelectionChangeCommand( + const HTMLEditor& aHTMLEditor, Command aCommand) { + if (mLastSelectionCommand != aCommand) { + return; + } + + // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`, + // it means that it didn't cause selection change. + if (!aHTMLEditor.SelectionRef().IsCollapsed() || + !aHTMLEditor.SelectionRef().RangeCount()) { + return; + } + + const auto caretPoint = + aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>(); + if (NS_WARN_IF(!caretPoint.IsSet())) { + return; + } + + if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) { + return; + } + + // If all styles are cleared or link style is explicitly set, we + // shouldn't reset them without caret move. + if (AreAllStylesCleared() || IsLinkStyleSet()) { + return; + } + // And if non-link styles are cleared or some styles are set, we + // shouldn't reset them too, but we may need to change the link + // style. + if (AreSomeStylesSet() || + (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) { + ClearLinkAndItsSpecifiedStyle(); + return; + } + + Reset(); + ClearLinkAndItsSpecifiedStyle(); +} + +void PendingStyles::OnSelectionChange(const HTMLEditor& aHTMLEditor, + int16_t aReason) { + // XXX: Selection currently generates bogus selection changed notifications + // XXX: (bug 140303). It can notify us when the selection hasn't actually + // XXX: changed, and it notifies us more than once for the same change. + // XXX: + // XXX: The following code attempts to work around the bogus notifications, + // XXX: and should probably be removed once bug 140303 is fixed. + // XXX: + // XXX: This code temporarily fixes the problem where clicking the mouse in + // XXX: the same location clears the type-in-state. + + const bool causedByFrameSelectionMoveCaret = + (aReason & (nsISelectionListener::KEYPRESS_REASON | + nsISelectionListener::COLLAPSETOSTART_REASON | + nsISelectionListener::COLLAPSETOEND_REASON)) && + !(aReason & nsISelectionListener::JS_REASON); + + Command lastSelectionCommand = mLastSelectionCommand; + if (causedByFrameSelectionMoveCaret) { + mLastSelectionCommand = Command::DoNothing; + } + + bool mouseEventFiredInLinkElement = false; + if (aReason & (nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::MOUSEUP_REASON)) { + MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::MOUSEUP_REASON)) != + (nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::MOUSEUP_REASON)); + bool& eventFiredInLinkElement = + aReason & nsISelectionListener::MOUSEDOWN_REASON + ? mMouseDownFiredInLinkElement + : mMouseUpFiredInLinkElement; + mouseEventFiredInLinkElement = eventFiredInLinkElement; + eventFiredInLinkElement = false; + } + + bool unlink = false; + bool resetAllStyles = true; + if (aHTMLEditor.SelectionRef().IsCollapsed() && + aHTMLEditor.SelectionRef().RangeCount()) { + const auto selectionStartPoint = + aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (MOZ_UNLIKELY(NS_WARN_IF(!selectionStartPoint.IsSet()))) { + return; + } + + if (mLastSelectionPoint == selectionStartPoint) { + // If all styles are cleared or link style is explicitly set, we + // shouldn't reset them without caret move. + if (AreAllStylesCleared() || IsLinkStyleSet()) { + return; + } + // And if non-link styles are cleared or some styles are set, we + // shouldn't reset them too, but we may need to change the link + // style. + if (AreSomeStylesSet() || + (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) { + resetAllStyles = false; + } + } + + RefPtr<Element> linkElement; + if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint, + getter_AddRefs(linkElement))) { + // If caret comes from outside of <a href> element, we should clear "link" + // style after reset. + if (causedByFrameSelectionMoveCaret) { + MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::MOUSEUP_REASON))); + // If caret is moves in a link per character, we should keep inserting + // new text to the link because user may want to keep extending the link + // text. Otherwise, e.g., using `End` or `Home` key. we should insert + // new text outside the link because it should be possible to user + // choose it, and this is similar to the other browsers. + switch (lastSelectionCommand) { + case Command::CharNext: + case Command::CharPrevious: + case Command::MoveLeft: + case Command::MoveLeft2: + case Command::MoveRight: + case Command::MoveRight2: + // If selection becomes collapsed, we should unlink new text. + if (!mLastSelectionPoint.IsSet()) { + unlink = true; + break; + } + // Special case, if selection isn't moved, it means that caret is + // positioned at start or end of an editing host. In this case, + // we can unlink it even with arrow key press. + // TODO: This does not work as expected for `ArrowLeft` key press + // at start of an editing host. + if (mLastSelectionPoint == selectionStartPoint) { + unlink = true; + break; + } + // Otherwise, if selection is moved in a link element, we should + // keep inserting new text into the link. Note that this is our + // traditional behavior, but different from the other browsers. + // If this breaks some web apps, we should change our behavior, + // but let's wait a report because our traditional behavior allows + // user to type text into start/end of a link only when user + // moves caret inside the link with arrow keys. + unlink = + !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf( + linkElement); + break; + default: + // If selection is moved without arrow keys, e.g., `Home` and + // `End`, we should not insert new text into the link element. + // This is important for web-compat especially when the link is + // the last content in the block. + unlink = true; + break; + } + } else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::MOUSEUP_REASON)) { + // If the corresponding mouse event is fired in a link element, + // we should keep treating inputting content as content in the link, + // but otherwise, i.e., clicked outside the link, we should stop + // treating inputting content as content in the link. + unlink = !mouseEventFiredInLinkElement; + } else if (aReason & nsISelectionListener::JS_REASON) { + // If this is caused by a call of Selection API or something similar + // API, we should not contain new inserting content to the link. + unlink = true; + } else { + switch (aHTMLEditor.GetEditAction()) { + case EditAction::eDeleteBackward: + case EditAction::eDeleteForward: + case EditAction::eDeleteSelection: + case EditAction::eDeleteToBeginningOfSoftLine: + case EditAction::eDeleteToEndOfSoftLine: + case EditAction::eDeleteWordBackward: + case EditAction::eDeleteWordForward: + // This selection change is caused by the editor and the edit + // action is deleting content at edge of a link, we shouldn't + // keep the link style for new inserted content. + unlink = true; + break; + default: + break; + } + } + } else if (mLastSelectionPoint == selectionStartPoint) { + return; + } + + mLastSelectionPoint = selectionStartPoint; + // We need to store only offset because referring child may be removed by + // we'll check the point later. + AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint); + } else { + if (aHTMLEditor.SelectionRef().RangeCount()) { + // If selection starts from a link, we shouldn't preserve the link style + // unless the range is entirely in the link. + EditorRawDOMRange firstRange(*aHTMLEditor.SelectionRef().GetRangeAt(0)); + if (firstRange.StartRef().IsInContentNode() && + HTMLEditUtils::IsContentInclusiveDescendantOfLink( + *firstRange.StartRef().ContainerAs<nsIContent>())) { + unlink = !HTMLEditUtils::IsRangeEntirelyInLink(firstRange); + } + } + mLastSelectionPoint.Clear(); + } + + if (resetAllStyles) { + Reset(); + if (unlink) { + ClearLinkAndItsSpecifiedStyle(); + } + return; + } + + if (unlink == IsExplicitlyLinkStyleCleared()) { + return; + } + + // Even if we shouldn't touch existing style, we need to set/clear only link + // style in some cases. + if (unlink) { + ClearLinkAndItsSpecifiedStyle(); + return; + } + CancelClearingStyle(*nsGkAtoms::a, nullptr); +} + +void PendingStyles::PreserveStyles( + const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve) { + for (const EditorInlineStyleAndValue& styleToPreserve : aStylesToPreserve) { + PreserveStyle(styleToPreserve.HTMLPropertyRef(), styleToPreserve.mAttribute, + styleToPreserve.mAttributeValue); + } +} + +void PendingStyles::PreserveStyle(nsStaticAtom& aHTMLProperty, + nsAtom* aAttribute, + const nsAString& aAttributeValueOrCSSValue) { + // special case for big/small, these nest + if (nsGkAtoms::big == &aHTMLProperty) { + mRelativeFontSize++; + return; + } + if (nsGkAtoms::small == &aHTMLProperty) { + mRelativeFontSize--; + return; + } + + Maybe<size_t> index = IndexOfPreservingStyle(aHTMLProperty, aAttribute); + if (index.isSome()) { + // If it's already set, update the value + mPreservingStyles[index.value()]->UpdateAttributeValueOrCSSValue( + aAttributeValueOrCSSValue); + return; + } + + // font-size and font-family need to be applied outer-most because height of + // outer inline elements of them are computed without these styles. E.g., + // background-color may be applied bottom-half of the text. Therefore, we + // need to apply the font styles first. + UniquePtr<PendingStyle> style = MakeUnique<PendingStyle>( + &aHTMLProperty, aAttribute, aAttributeValueOrCSSValue); + if (&aHTMLProperty == nsGkAtoms::font && aAttribute != nsGkAtoms::bgcolor) { + MOZ_ASSERT(aAttribute == nsGkAtoms::color || + aAttribute == nsGkAtoms::face || aAttribute == nsGkAtoms::size); + mPreservingStyles.InsertElementAt(0, std::move(style)); + } else { + mPreservingStyles.AppendElement(std::move(style)); + } + + CancelClearingStyle(aHTMLProperty, aAttribute); +} + +void PendingStyles::ClearStyles( + const nsTArray<EditorInlineStyle>& aStylesToClear) { + for (const EditorInlineStyle& styleToClear : aStylesToClear) { + if (styleToClear.IsStyleToClearAllInlineStyles()) { + ClearAllStyles(); + return; + } + if (styleToClear.mHTMLProperty == nsGkAtoms::href || + styleToClear.mHTMLProperty == nsGkAtoms::name) { + ClearStyleInternal(nsGkAtoms::a, nullptr); + } else { + ClearStyleInternal(styleToClear.mHTMLProperty, styleToClear.mAttribute); + } + } +} + +void PendingStyles::ClearStyleInternal( + nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, + SpecifiedStyle aSpecifiedStyle /* = SpecifiedStyle::Preserve */) { + if (IsStyleCleared(aHTMLProperty, aAttribute)) { + return; + } + + CancelPreservingStyle(aHTMLProperty, aAttribute); + + mClearingStyles.AppendElement(MakeUnique<PendingStyle>( + aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle)); +} + +void PendingStyles::TakeAllPreservedStyles( + nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues) { + aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() + + mPreservingStyles.Length()); + for (const UniquePtr<PendingStyle>& preservedStyle : mPreservingStyles) { + aOutStylesAndValues.AppendElement( + preservedStyle->GetAttribute() + ? EditorInlineStyleAndValue( + *preservedStyle->GetTag(), *preservedStyle->GetAttribute(), + preservedStyle->AttributeValueOrCSSValueRef()) + : EditorInlineStyleAndValue(*preservedStyle->GetTag())); + } + mPreservingStyles.Clear(); +} + +/** + * TakeRelativeFontSize() hands back relative font value, which is then + * cleared out. + */ +int32_t PendingStyles::TakeRelativeFontSize() { + int32_t relSize = mRelativeFontSize; + mRelativeFontSize = 0; + return relSize; +} + +PendingStyleState PendingStyles::GetStyleState( + nsStaticAtom& aHTMLProperty, nsAtom* aAttribute /* = nullptr */, + nsString* aOutNewAttributeValueOrCSSValue /* = nullptr */) const { + if (IndexOfPreservingStyle(aHTMLProperty, aAttribute, + aOutNewAttributeValueOrCSSValue) + .isSome()) { + return PendingStyleState::BeingPreserved; + } + + if (IsStyleCleared(&aHTMLProperty, aAttribute)) { + return PendingStyleState::BeingCleared; + } + + return PendingStyleState::NotUpdated; +} + +void PendingStyles::CancelPreservingStyle(nsStaticAtom* aHTMLProperty, + nsAtom* aAttribute) { + if (!aHTMLProperty) { + mPreservingStyles.Clear(); + mRelativeFontSize = 0; + return; + } + Maybe<size_t> index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute); + if (index.isSome()) { + mPreservingStyles.RemoveElementAt(index.value()); + } +} + +void PendingStyles::CancelClearingStyle(nsStaticAtom& aHTMLProperty, + nsAtom* aAttribute) { + Maybe<size_t> index = + IndexOfStyleInArray(&aHTMLProperty, aAttribute, nullptr, mClearingStyles); + if (index.isSome()) { + mClearingStyles.RemoveElementAt(index.value()); + } +} + +Maybe<size_t> PendingStyles::IndexOfStyleInArray( + nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue, + const nsTArray<UniquePtr<PendingStyle>>& aArray) { + if (aAttribute == nsGkAtoms::_empty) { + aAttribute = nullptr; + } + for (size_t i : IntegerRange(aArray.Length())) { + const UniquePtr<PendingStyle>& item = aArray[i]; + if (item->GetTag() == aHTMLProperty && item->GetAttribute() == aAttribute) { + if (aOutValue) { + *aOutValue = item->AttributeValueOrCSSValueRef(); + } + return Some(i); + } + } + return Nothing(); +} + +} // namespace mozilla |