/* -*- 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 #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(); 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(); 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(); 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 linkElement; if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint, getter_AddRefs(linkElement))) { // If caret comes from outside of 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())) { 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& 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 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 style = MakeUnique( &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& 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( aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle)); } void PendingStyles::TakeAllPreservedStyles( nsTArray& aOutStylesAndValues) { aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() + mPreservingStyles.Length()); for (const UniquePtr& 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 index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute); if (index.isSome()) { mPreservingStyles.RemoveElementAt(index.value()); } } void PendingStyles::CancelClearingStyle(nsStaticAtom& aHTMLProperty, nsAtom* aAttribute) { Maybe index = IndexOfStyleInArray(&aHTMLProperty, aAttribute, nullptr, mClearingStyles); if (index.isSome()) { mClearingStyles.RemoveElementAt(index.value()); } } Maybe PendingStyles::IndexOfStyleInArray( nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue, const nsTArray>& aArray) { if (aAttribute == nsGkAtoms::_empty) { aAttribute = nullptr; } for (size_t i : IntegerRange(aArray.Length())) { const UniquePtr& item = aArray[i]; if (item->GetTag() == aHTMLProperty && item->GetAttribute() == aAttribute) { if (aOutValue) { *aOutValue = item->AttributeValueOrCSSValueRef(); } return Some(i); } } return Nothing(); } } // namespace mozilla