зеркало из https://github.com/mozilla/gecko-dev.git
504 строки
18 KiB
C++
504 строки
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
|