Files
tubestation/editor/libeditor/PendingStyles.cpp
Masayuki Nakano 1c1025a6e6 Bug 1814337 - Fix bugs at applying multiple styles to collapsed Selection r=m_kato
There are 3 bugs.

One is, `PendingStyles::TakeAllPreservedStyles` returns styles in reversed order
of setting styles, this unexpected order causes that `<font>` element is placed
innermost instead of outermost, thus, background color may be applied to shorter
area than text.  Unfortunately, Chrome applies the style in fixed order, but
using same order as setting the styles is reasonable for us because we use
same code to apply multiple styles once.

Another is, `AutoInlineStyleSetter` extends the range to apply new style to
parents even if the given point is not in start or end of the first container.
Current code works fine if there is only one text node in an inline element,
but after un-setting a style, text nodes may be keep split.  And also when we
apply new style, we create an empty text node for a placeholder of new text,
therefore, the range may be extended from the empty text node to before/after
its previous/next text node.  Note that the latter case needs to work with
invisible things more, but this bug needs to be fixed ASAP due to the dead line,
and invisible things not so appear in inline elements.  Therefore, it's okay
to leave it.

The other is, the delete handlers do not clear the cached styles (cached when
`HTMLEditor` starts handling it).  If it's caused by a `Backspace` key press
in start of a block, `HTMLEditor` caches the style at caret (right paragraph),
then, collapse `Selection` to end of the ex-left block where may have different
style, then, when user types new text there, the cached style is applied into
to the new text **too**.  Therefore, if `Selection` was collapsed and handling
a `Backspace` key press, the cached styles should be forgotten.

Depends on D168640

Differential Revision: https://phabricator.services.mozilla.com/D168865
2023-02-07 14:52:02 +00:00

504 lines
18 KiB
C++

/* -*- 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