tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PendingStyles.cpp (18736B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 #include "PendingStyles.h"
      7 
      8 #include <stddef.h>
      9 
     10 #include "EditAction.h"
     11 #include "EditorBase.h"
     12 #include "HTMLEditHelpers.h"  // for EditorInlineStyle, EditorInlineStyleAndValue
     13 #include "HTMLEditor.h"
     14 #include "HTMLEditUtils.h"
     15 
     16 #include "mozilla/mozalloc.h"
     17 #include "mozilla/dom/AncestorIterator.h"
     18 #include "mozilla/dom/MouseEvent.h"
     19 #include "mozilla/dom/Selection.h"
     20 
     21 #include "nsDebug.h"
     22 #include "nsError.h"
     23 #include "nsGkAtoms.h"
     24 #include "nsINode.h"
     25 #include "nsISupports.h"
     26 #include "nsISupportsImpl.h"
     27 #include "nsReadableUtils.h"
     28 #include "nsString.h"
     29 #include "nsTArray.h"
     30 
     31 namespace mozilla {
     32 
     33 using namespace dom;
     34 
     35 /********************************************************************
     36 * mozilla::PendingStyle
     37 *******************************************************************/
     38 
     39 EditorInlineStyle PendingStyle::ToInlineStyle() const {
     40  return mTag ? EditorInlineStyle(*mTag, mAttribute)
     41              : EditorInlineStyle::RemoveAllStyles();
     42 }
     43 
     44 EditorInlineStyleAndValue PendingStyle::ToInlineStyleAndValue() const {
     45  MOZ_ASSERT(mTag);
     46  return mAttribute ? EditorInlineStyleAndValue(*mTag, *mAttribute,
     47                                                mAttributeValueOrCSSValue)
     48                    : EditorInlineStyleAndValue(*mTag);
     49 }
     50 
     51 /********************************************************************
     52 * mozilla::PendingStyleCache
     53 *******************************************************************/
     54 
     55 EditorInlineStyle PendingStyleCache::ToInlineStyle() const {
     56  return EditorInlineStyle(mTag, mAttribute);
     57 }
     58 
     59 /********************************************************************
     60 * mozilla::PendingStyles
     61 *******************************************************************/
     62 
     63 NS_IMPL_CYCLE_COLLECTION_CLASS(PendingStyles)
     64 
     65 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PendingStyles)
     66  NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint)
     67 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
     68 
     69 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PendingStyles)
     70  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint)
     71 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
     72 
     73 nsresult PendingStyles::UpdateSelState(const HTMLEditor& aHTMLEditor) {
     74  if (!aHTMLEditor.SelectionRef().IsCollapsed()) {
     75    return NS_OK;
     76  }
     77 
     78  mLastSelectionPoint =
     79      aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
     80  if (!mLastSelectionPoint.IsSet()) {
     81    return NS_ERROR_FAILURE;
     82  }
     83  // We need to store only offset because referring child may be removed by
     84  // we'll check the point later.
     85  AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
     86  return NS_OK;
     87 }
     88 
     89 void PendingStyles::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) {
     90  MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown ||
     91             aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp);
     92  bool& eventFiredInLinkElement =
     93      aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown
     94          ? mMouseDownFiredInLinkElement
     95          : mMouseUpFiredInLinkElement;
     96  eventFiredInLinkElement = false;
     97  if (aMouseDownOrUpEvent.DefaultPrevented()) {
     98    return;
     99  }
    100  // If mouse button is down or up in a link element, we shouldn't unlink
    101  // it when we get a notification of selection change.
    102  EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget();
    103  if (NS_WARN_IF(!target)) {
    104    return;
    105  }
    106  nsIContent* targetContent = nsIContent::FromEventTarget(target);
    107  if (NS_WARN_IF(!targetContent)) {
    108    return;
    109  }
    110  eventFiredInLinkElement =
    111      HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent);
    112 }
    113 
    114 void PendingStyles::PreHandleSelectionChangeCommand(Command aCommand) {
    115  mLastSelectionCommand = aCommand;
    116 }
    117 
    118 void PendingStyles::PostHandleSelectionChangeCommand(
    119    const HTMLEditor& aHTMLEditor, Command aCommand) {
    120  if (mLastSelectionCommand != aCommand) {
    121    return;
    122  }
    123 
    124  // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
    125  // it means that it didn't cause selection change.
    126  if (!aHTMLEditor.SelectionRef().IsCollapsed() ||
    127      !aHTMLEditor.SelectionRef().RangeCount()) {
    128    return;
    129  }
    130 
    131  const auto caretPoint =
    132      aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>();
    133  if (NS_WARN_IF(!caretPoint.IsSet())) {
    134    return;
    135  }
    136 
    137  if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
    138    return;
    139  }
    140 
    141  // If all styles are cleared or link style is explicitly set, we
    142  // shouldn't reset them without caret move.
    143  if (AreAllStylesCleared() || IsLinkStyleSet()) {
    144    return;
    145  }
    146  // And if non-link styles are cleared or some styles are set, we
    147  // shouldn't reset them too, but we may need to change the link
    148  // style.
    149  if (AreSomeStylesSet() ||
    150      (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
    151    ClearLinkAndItsSpecifiedStyle();
    152    return;
    153  }
    154 
    155  Reset();
    156  ClearLinkAndItsSpecifiedStyle();
    157 }
    158 
    159 void PendingStyles::OnSelectionChange(const HTMLEditor& aHTMLEditor,
    160                                      int16_t aReason) {
    161  // XXX: Selection currently generates bogus selection changed notifications
    162  // XXX: (bug 140303). It can notify us when the selection hasn't actually
    163  // XXX: changed, and it notifies us more than once for the same change.
    164  // XXX:
    165  // XXX: The following code attempts to work around the bogus notifications,
    166  // XXX: and should probably be removed once bug 140303 is fixed.
    167  // XXX:
    168  // XXX: This code temporarily fixes the problem where clicking the mouse in
    169  // XXX: the same location clears the type-in-state.
    170 
    171  const bool causedByFrameSelectionMoveCaret =
    172      (aReason & (nsISelectionListener::KEYPRESS_REASON |
    173                  nsISelectionListener::COLLAPSETOSTART_REASON |
    174                  nsISelectionListener::COLLAPSETOEND_REASON)) &&
    175      !(aReason & nsISelectionListener::JS_REASON);
    176 
    177  Command lastSelectionCommand = mLastSelectionCommand;
    178  if (causedByFrameSelectionMoveCaret) {
    179    mLastSelectionCommand = Command::DoNothing;
    180  }
    181 
    182  bool mouseEventFiredInLinkElement = false;
    183  if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
    184                 nsISelectionListener::MOUSEUP_REASON)) {
    185    MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
    186                           nsISelectionListener::MOUSEUP_REASON)) !=
    187               (nsISelectionListener::MOUSEDOWN_REASON |
    188                nsISelectionListener::MOUSEUP_REASON));
    189    bool& eventFiredInLinkElement =
    190        aReason & nsISelectionListener::MOUSEDOWN_REASON
    191            ? mMouseDownFiredInLinkElement
    192            : mMouseUpFiredInLinkElement;
    193    mouseEventFiredInLinkElement = eventFiredInLinkElement;
    194    eventFiredInLinkElement = false;
    195  }
    196 
    197  bool unlink = false;
    198  bool resetAllStyles = true;
    199  if (aHTMLEditor.SelectionRef().IsCollapsed() &&
    200      aHTMLEditor.SelectionRef().RangeCount()) {
    201    const auto selectionStartPoint =
    202        aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
    203    if (MOZ_UNLIKELY(NS_WARN_IF(!selectionStartPoint.IsSet()))) {
    204      return;
    205    }
    206 
    207    if (mLastSelectionPoint == selectionStartPoint) {
    208      // If all styles are cleared or link style is explicitly set, we
    209      // shouldn't reset them without caret move.
    210      if (AreAllStylesCleared() || IsLinkStyleSet()) {
    211        return;
    212      }
    213      // And if non-link styles are cleared or some styles are set, we
    214      // shouldn't reset them too, but we may need to change the link
    215      // style.
    216      if (AreSomeStylesSet() ||
    217          (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
    218        resetAllStyles = false;
    219      }
    220    }
    221 
    222    RefPtr<Element> linkElement;
    223    if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint,
    224                                           getter_AddRefs(linkElement))) {
    225      // If caret comes from outside of <a href> element, we should clear "link"
    226      // style after reset.
    227      if (causedByFrameSelectionMoveCaret) {
    228        MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON |
    229                                nsISelectionListener::MOUSEUP_REASON)));
    230        // If caret is moves in a link per character, we should keep inserting
    231        // new text to the link because user may want to keep extending the link
    232        // text.  Otherwise, e.g., using `End` or `Home` key. we should insert
    233        // new text outside the link because it should be possible to user
    234        // choose it, and this is similar to the other browsers.
    235        switch (lastSelectionCommand) {
    236          case Command::CharNext:
    237          case Command::CharPrevious:
    238          case Command::MoveLeft:
    239          case Command::MoveLeft2:
    240          case Command::MoveRight:
    241          case Command::MoveRight2:
    242            // If selection becomes collapsed, we should unlink new text.
    243            if (!mLastSelectionPoint.IsSet()) {
    244              unlink = true;
    245              break;
    246            }
    247            // Special case, if selection isn't moved, it means that caret is
    248            // positioned at start or end of an editing host.  In this case,
    249            // we can unlink it even with arrow key press.
    250            // TODO: This does not work as expected for `ArrowLeft` key press
    251            //       at start of an editing host.
    252            if (mLastSelectionPoint == selectionStartPoint) {
    253              unlink = true;
    254              break;
    255            }
    256            // Otherwise, if selection is moved in a link element, we should
    257            // keep inserting new text into the link.  Note that this is our
    258            // traditional behavior, but different from the other browsers.
    259            // If this breaks some web apps, we should change our behavior,
    260            // but let's wait a report because our traditional behavior allows
    261            // user to type text into start/end of a link only when user
    262            // moves caret inside the link with arrow keys.
    263            unlink =
    264                !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
    265                    linkElement);
    266            break;
    267          default:
    268            // If selection is moved without arrow keys, e.g., `Home` and
    269            // `End`, we should not insert new text into the link element.
    270            // This is important for web-compat especially when the link is
    271            // the last content in the block.
    272            unlink = true;
    273            break;
    274        }
    275      } else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
    276                            nsISelectionListener::MOUSEUP_REASON)) {
    277        // If the corresponding mouse event is fired in a link element,
    278        // we should keep treating inputting content as content in the link,
    279        // but otherwise, i.e., clicked outside the link, we should stop
    280        // treating inputting content as content in the link.
    281        unlink = !mouseEventFiredInLinkElement;
    282      } else if (aReason & nsISelectionListener::JS_REASON) {
    283        // If this is caused by a call of Selection API or something similar
    284        // API, we should not contain new inserting content to the link.
    285        unlink = true;
    286      } else {
    287        switch (aHTMLEditor.GetEditAction()) {
    288          case EditAction::eDeleteBackward:
    289          case EditAction::eDeleteForward:
    290          case EditAction::eDeleteSelection:
    291          case EditAction::eDeleteToBeginningOfSoftLine:
    292          case EditAction::eDeleteToEndOfSoftLine:
    293          case EditAction::eDeleteWordBackward:
    294          case EditAction::eDeleteWordForward:
    295            // This selection change is caused by the editor and the edit
    296            // action is deleting content at edge of a link, we shouldn't
    297            // keep the link style for new inserted content.
    298            unlink = true;
    299            break;
    300          default:
    301            break;
    302        }
    303      }
    304    } else if (mLastSelectionPoint == selectionStartPoint) {
    305      return;
    306    }
    307 
    308    mLastSelectionPoint = selectionStartPoint;
    309    // We need to store only offset because referring child may be removed by
    310    // we'll check the point later.
    311    AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
    312  } else {
    313    if (aHTMLEditor.SelectionRef().RangeCount()) {
    314      // If selection starts from a link, we shouldn't preserve the link style
    315      // unless the range is entirely in the link.
    316      EditorRawDOMRange firstRange(*aHTMLEditor.SelectionRef().GetRangeAt(0));
    317      if (firstRange.StartRef().IsInContentNode() &&
    318          HTMLEditUtils::IsContentInclusiveDescendantOfLink(
    319              *firstRange.StartRef().ContainerAs<nsIContent>())) {
    320        unlink = !HTMLEditUtils::IsRangeEntirelyInLink(firstRange);
    321      }
    322    }
    323    mLastSelectionPoint.Clear();
    324  }
    325 
    326  if (resetAllStyles) {
    327    Reset();
    328    if (unlink) {
    329      ClearLinkAndItsSpecifiedStyle();
    330    }
    331    return;
    332  }
    333 
    334  if (unlink == IsExplicitlyLinkStyleCleared()) {
    335    return;
    336  }
    337 
    338  // Even if we shouldn't touch existing style, we need to set/clear only link
    339  // style in some cases.
    340  if (unlink) {
    341    ClearLinkAndItsSpecifiedStyle();
    342    return;
    343  }
    344  CancelClearingStyle(*nsGkAtoms::a, nullptr);
    345 }
    346 
    347 void PendingStyles::PreserveStyles(
    348    const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve) {
    349  for (const EditorInlineStyleAndValue& styleToPreserve : aStylesToPreserve) {
    350    PreserveStyle(styleToPreserve.HTMLPropertyRef(), styleToPreserve.mAttribute,
    351                  styleToPreserve.mAttributeValue);
    352  }
    353 }
    354 
    355 void PendingStyles::PreserveStyle(nsStaticAtom& aHTMLProperty,
    356                                  nsAtom* aAttribute,
    357                                  const nsAString& aAttributeValueOrCSSValue) {
    358  // special case for big/small, these nest
    359  if (nsGkAtoms::big == &aHTMLProperty) {
    360    mRelativeFontSize++;
    361    return;
    362  }
    363  if (nsGkAtoms::small == &aHTMLProperty) {
    364    mRelativeFontSize--;
    365    return;
    366  }
    367 
    368  Maybe<size_t> index = IndexOfPreservingStyle(aHTMLProperty, aAttribute);
    369  if (index.isSome()) {
    370    // If it's already set, update the value
    371    mPreservingStyles[index.value()]->UpdateAttributeValueOrCSSValue(
    372        aAttributeValueOrCSSValue);
    373    return;
    374  }
    375 
    376  // font-size and font-family need to be applied outer-most because height of
    377  // outer inline elements of them are computed without these styles.  E.g.,
    378  // background-color may be applied bottom-half of the text.  Therefore, we
    379  // need to apply the font styles first.
    380  UniquePtr<PendingStyle> style = MakeUnique<PendingStyle>(
    381      &aHTMLProperty, aAttribute, aAttributeValueOrCSSValue);
    382  if (&aHTMLProperty == nsGkAtoms::font && aAttribute != nsGkAtoms::bgcolor) {
    383    MOZ_ASSERT(aAttribute == nsGkAtoms::color ||
    384               aAttribute == nsGkAtoms::face || aAttribute == nsGkAtoms::size);
    385    mPreservingStyles.InsertElementAt(0, std::move(style));
    386  } else {
    387    mPreservingStyles.AppendElement(std::move(style));
    388  }
    389 
    390  CancelClearingStyle(aHTMLProperty, aAttribute);
    391 }
    392 
    393 void PendingStyles::ClearStyles(
    394    const nsTArray<EditorInlineStyle>& aStylesToClear) {
    395  for (const EditorInlineStyle& styleToClear : aStylesToClear) {
    396    if (styleToClear.IsStyleToClearAllInlineStyles()) {
    397      ClearAllStyles();
    398      return;
    399    }
    400    if (styleToClear.mHTMLProperty == nsGkAtoms::href ||
    401        styleToClear.mHTMLProperty == nsGkAtoms::name) {
    402      ClearStyleInternal(nsGkAtoms::a, nullptr);
    403    } else {
    404      ClearStyleInternal(styleToClear.mHTMLProperty, styleToClear.mAttribute);
    405    }
    406  }
    407 }
    408 
    409 void PendingStyles::ClearStyleInternal(
    410    nsStaticAtom* aHTMLProperty, nsAtom* aAttribute,
    411    SpecifiedStyle aSpecifiedStyle /* = SpecifiedStyle::Preserve */) {
    412  if (IsStyleCleared(aHTMLProperty, aAttribute)) {
    413    return;
    414  }
    415 
    416  CancelPreservingStyle(aHTMLProperty, aAttribute);
    417 
    418  mClearingStyles.AppendElement(MakeUnique<PendingStyle>(
    419      aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle));
    420 }
    421 
    422 void PendingStyles::TakeAllPreservedStyles(
    423    nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues) {
    424  aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() +
    425                                  mPreservingStyles.Length());
    426  for (const UniquePtr<PendingStyle>& preservedStyle : mPreservingStyles) {
    427    aOutStylesAndValues.AppendElement(
    428        preservedStyle->GetAttribute()
    429            ? EditorInlineStyleAndValue(
    430                  *preservedStyle->GetTag(), *preservedStyle->GetAttribute(),
    431                  preservedStyle->AttributeValueOrCSSValueRef())
    432            : EditorInlineStyleAndValue(*preservedStyle->GetTag()));
    433  }
    434  mPreservingStyles.Clear();
    435 }
    436 
    437 /**
    438 * TakeRelativeFontSize() hands back relative font value, which is then
    439 * cleared out.
    440 */
    441 int32_t PendingStyles::TakeRelativeFontSize() {
    442  int32_t relSize = mRelativeFontSize;
    443  mRelativeFontSize = 0;
    444  return relSize;
    445 }
    446 
    447 PendingStyleState PendingStyles::GetStyleState(
    448    nsStaticAtom& aHTMLProperty, nsAtom* aAttribute /* = nullptr */,
    449    nsString* aOutNewAttributeValueOrCSSValue /* = nullptr */) const {
    450  if (IndexOfPreservingStyle(aHTMLProperty, aAttribute,
    451                             aOutNewAttributeValueOrCSSValue)
    452          .isSome()) {
    453    return PendingStyleState::BeingPreserved;
    454  }
    455 
    456  if (IsStyleCleared(&aHTMLProperty, aAttribute)) {
    457    return PendingStyleState::BeingCleared;
    458  }
    459 
    460  return PendingStyleState::NotUpdated;
    461 }
    462 
    463 void PendingStyles::CancelPreservingStyle(nsStaticAtom* aHTMLProperty,
    464                                          nsAtom* aAttribute) {
    465  if (!aHTMLProperty) {
    466    mPreservingStyles.Clear();
    467    mRelativeFontSize = 0;
    468    return;
    469  }
    470  Maybe<size_t> index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute);
    471  if (index.isSome()) {
    472    mPreservingStyles.RemoveElementAt(index.value());
    473  }
    474 }
    475 
    476 void PendingStyles::CancelClearingStyle(nsStaticAtom& aHTMLProperty,
    477                                        nsAtom* aAttribute) {
    478  Maybe<size_t> index =
    479      IndexOfStyleInArray(&aHTMLProperty, aAttribute, nullptr, mClearingStyles);
    480  if (index.isSome()) {
    481    mClearingStyles.RemoveElementAt(index.value());
    482  }
    483 }
    484 
    485 Maybe<size_t> PendingStyles::IndexOfStyleInArray(
    486    nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue,
    487    const nsTArray<UniquePtr<PendingStyle>>& aArray) {
    488  if (aAttribute == nsGkAtoms::_empty) {
    489    aAttribute = nullptr;
    490  }
    491  for (size_t i : IntegerRange(aArray.Length())) {
    492    const UniquePtr<PendingStyle>& item = aArray[i];
    493    if (item->GetTag() == aHTMLProperty && item->GetAttribute() == aAttribute) {
    494      if (aOutValue) {
    495        *aOutValue = item->AttributeValueOrCSSValueRef();
    496      }
    497      return Some(i);
    498    }
    499  }
    500  return Nothing();
    501 }
    502 
    503 }  // namespace mozilla